diff --git a/cmd/mrmodel/cli.go b/cmd/mrmodel/cli.go index 1ee74e34..e3aae634 100644 --- a/cmd/mrmodel/cli.go +++ b/cmd/mrmodel/cli.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "flag" "fmt" @@ -238,7 +239,7 @@ func runBitbucket(args []string) error { return fmt.Errorf("bitbucket provider creation failed: %w", errProv) } - unifiedArtifact, err := mrModel.BuildBitbucketArtifact(provider, prID, prURL, *outDir) + unifiedArtifact, err := mrModel.BuildBitbucketArtifact(context.Background(), provider, prID, prURL, *outDir) if err != nil { return err } diff --git a/cmd/mrmodel/lib/bitbucket.go b/cmd/mrmodel/lib/bitbucket.go index 6ba92600..54a6194a 100644 --- a/cmd/mrmodel/lib/bitbucket.go +++ b/cmd/mrmodel/lib/bitbucket.go @@ -15,34 +15,34 @@ import ( rm "github.com/livereview/internal/reviewmodel" ) -func (m *MrModelImpl) FetchBitbucketData(provider interface{}, prID string, prURL string) (details *providers.MergeRequestDetails, diffs string, commits interface{}, comments interface{}, err error) { +func (m *MrModelImpl) FetchBitbucketData(ctx context.Context, provider interface{}, prID string, prURL string) (details *providers.MergeRequestDetails, diffs string, commits interface{}, comments interface{}, err error) { // Type assertion for Bitbucket provider bbProvider, ok := provider.(interface { GetMergeRequestDetails(ctx context.Context, prURL string) (*providers.MergeRequestDetails, error) - GetPullRequestDiff(prID string) (string, error) - GetPullRequestCommits(prID string) ([]bitbucket.BitbucketCommit, error) - GetPullRequestComments(prID string) ([]bitbucket.BitbucketComment, error) + GetPullRequestDiff(ctx context.Context, prID string) (string, error) + GetPullRequestCommits(ctx context.Context, prID string) ([]bitbucket.BitbucketCommit, error) + GetPullRequestComments(ctx context.Context, prID string) ([]bitbucket.BitbucketComment, error) }) if !ok { return nil, "", nil, nil, fmt.Errorf("invalid Bitbucket provider") } - details, err = bbProvider.GetMergeRequestDetails(context.Background(), prURL) + details, err = bbProvider.GetMergeRequestDetails(ctx, prURL) if err != nil { return nil, "", nil, nil, fmt.Errorf("GetMergeRequestDetails failed: %w", err) } - diffs, err = bbProvider.GetPullRequestDiff(prID) + diffs, err = bbProvider.GetPullRequestDiff(ctx, prID) if err != nil { return nil, "", nil, nil, fmt.Errorf("failed to get MR changes: %w", err) } - commits, err = bbProvider.GetPullRequestCommits(prID) + commits, err = bbProvider.GetPullRequestCommits(ctx, prID) if err != nil { return nil, "", nil, nil, fmt.Errorf("GetPullRequestCommits failed: %w", err) } - comments, err = bbProvider.GetPullRequestComments(prID) + comments, err = bbProvider.GetPullRequestComments(ctx, prID) if err != nil { return nil, "", nil, nil, fmt.Errorf("GetPullRequestComments failed: %w", err) } @@ -50,8 +50,8 @@ func (m *MrModelImpl) FetchBitbucketData(provider interface{}, prID string, prUR return details, diffs, commits, comments, nil } -func (m *MrModelImpl) BuildBitbucketArtifact(provider *bitbucket.BitbucketProvider, prID, prURL, outDir string) (*UnifiedArtifact, error) { - details, diffs, commitsIface, commentsIface, err := m.FetchBitbucketData(provider, prID, prURL) +func (m *MrModelImpl) BuildBitbucketArtifact(ctx context.Context, provider *bitbucket.BitbucketProvider, prID, prURL, outDir string) (*UnifiedArtifact, error) { + details, diffs, commitsIface, commentsIface, err := m.FetchBitbucketData(ctx, provider, prID, prURL) if err != nil { return nil, err } diff --git a/docs/security/osv-scanner-fix.md b/docs/security/osv-scanner-fix.md new file mode 100644 index 00000000..21625ce5 --- /dev/null +++ b/docs/security/osv-scanner-fix.md @@ -0,0 +1,76 @@ +# How to fix OSV scanner vulnerabilities + +This document describes how to run the same security scan locally and how to fix the vulnerabilities. + + +## How to run OSV scanner Localy + +Generate a security report by running the following command: + +```bash +make security-osv +``` + +The `make security-osv` Makefile target runs `osv-scanner` recursively across the repository and writes a dated JSON report to `security_issues/`. + +The output of the scan is: + +``` +Scanning dir . +Warning: plugin transitivedependency/pomxml can be risky when run on untrusted artifacts. Please ensure you trust the source code and artifacts before proceeding. +Starting filesystem walk for root: / +Scanned /home/gk/hex/LiveReview/go.mod file and found 148 packages +Scanned /home/gk/hex/LiveReview/internal/prompts/vendor/cmd file and found 0 packages +Scanned /home/gk/hex/LiveReview/extension/livereview/package-lock.json file and found 378 packages +Scanned /home/gk/hex/LiveReview/ui/package-lock.json file and found 1287 packages +End status: 407 dirs visited, 1971 inodes visited, 4 Extract calls, 602.472549ms elapsed, 602.472606ms wall time +Wrote security_issues/osv-scanner-05-04-2026.json +Updated security_issues/osv-scanner-latest.json +``` + +So, osv-scan report will be generaed. + +This report will have all the vulnerabilities found in the repository. + +Ideally, the report should be empty. + +```json +{ + "results": [], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": null + } + } +} +``` + + +## How to fix vulnerabilities + +1. Select the osv-scanner report. +2. Add to AI prompt and ask to fix the vulnerabilities. +3. AI will fix the vulnerabilities by updating the dependencies. +4. Run `make security-osv` again to verify that the vulnerabilities are fixed. +5. If vulnerabilities are still present, repeat the process by actually looking into each vulnerability and fix it manually. + +## Verify the fix + +If all vulnerabilities are fixed, the `make security-osv` command will not find any vulnerabilities and the report will be empty. + +```json +{ + "results": [], + "experimental_config": { + "licenses": { + "summary": false, + "allowlist": null + } + } +} +``` + +Now Verify by running ui, server and extension. +This is for local verification wheather the change in package.json or go.mod is correct or not. + diff --git a/internal/ai/langchain/json_repair_integration_test.go b/internal/ai/langchain/json_repair_integration_test.go index bf24ad88..b465dfd7 100644 --- a/internal/ai/langchain/json_repair_integration_test.go +++ b/internal/ai/langchain/json_repair_integration_test.go @@ -53,3 +53,435 @@ func TestParseResponseWithRepair_AppliesSanitizationAfterRepair(t *testing.T) { t.Fatalf("expected suggestion email to be redacted, got: %s", result.Comments[0].Suggestions[0]) } } + +// TestLineIsDeleted_FormattedHunkContent verifies that lineIsDeleted correctly +// identifies deleted lines when hunk.Content is in the pre-formatted +// "OLD | NEW | CONTENT" table format produced by formatHunkWithLineNumbers. +// +// This is the format that lineIsDeleted actually receives at runtime: +// - formatHunkWithLineNumbers runs first (during ReviewCodeWithBatching) +// - lineIsDeleted runs later (during parseResponse) +// +// The bug: the original implementation checked HasPrefix(line, "-") which never +// matches formatted rows like "844 | | -func ..." that start with a digit. +func TestLineIsDeleted_FormattedHunkContent(t *testing.T) { + provider := &LangchainProvider{} + + // This is the exact format produced by formatSingleHunk for the diff: + // @@ -841,7 +886,6 @@ + // context line (841→886) + // context line (842→887) + // context line (843→888) // setDefaultColumnNames... + // -func (d *Deidentifier) setDefaultColumnNames(...) [old:844, deleted] + // context line (845→889) + // context line (846→890) + // context line (847→891) + formattedContent := "@@ -841,7 +886,6 @@ func (d *Deidentifier) selectBestType\n" + + "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + "841 | 886 | \n" + + "842 | 887 | \n" + + "843 | 888 | // setDefaultColumnNames generates default column names if not provided\n" + + "844 | | -func (d *Deidentifier) setDefaultColumnNames(config *slicesConfig) error {\n" + + "845 | 889 | \tif len(config.columnNames) == 0 {\n" + + "846 | 890 | \t\tconfig.columnNames = make([]string, config.numCols)\n" + + "847 | 891 | \t\tfor i := 0; i < config.numCols; i++ {\n" + + hunk := models.DiffHunk{ + OldStartLine: 841, + OldLineCount: 7, + NewStartLine: 886, + NewLineCount: 6, + Content: formattedContent, + } + + // Line 844 is the deleted line (OLD=844, NEW=blank). + // With the broken implementation: HasPrefix("844 | | -func...", "-") == false + // → returns false (WRONG). With the fix: parses the table and returns true. + if !provider.lineIsDeleted(844, hunk) { + t.Errorf("lineIsDeleted(844) = false, want true: line 844 is a deleted line in the formatted hunk") + } + + // Line 843 is a context line — must NOT be flagged as deleted. + if provider.lineIsDeleted(843, hunk) { + t.Errorf("lineIsDeleted(843) = true, want false: line 843 is a context line") + } + + // Line 886 is a context line on the new side — must NOT be flagged as deleted. + if provider.lineIsDeleted(886, hunk) { + t.Errorf("lineIsDeleted(886) = true, want false: line 886 is a context (new-side) line") + } +} + +// TestLineIsDeleted_AllCommentTypes exercises all four comment types the LLM +// produces and that PostComment routes to different Bitbucket API payloads. +// +// From the actual PR "Deleted and Added PR" (deidentify.go): +// +// Type 1 – general comment : FilePath="", Line=0 → PostGeneralComment +// Type 2 – deleted-line comment : IsDeletedLine=true → "from" field +// Type 3 – added-line comment : IsDeletedLine=false → "to" field +// Type 4 – reply on comment thread : IsDeletedLine=false (reply) → parent ID +// +// lineIsDeleted is only responsible for Types 2 vs 3; Types 1 and 4 never reach it. +func TestLineIsDeleted_AllCommentTypes(t *testing.T) { + provider := &LangchainProvider{} + + // Hunk 1 from the actual log: @@ -45,6 +45,18 @@ type Table struct + // All LLM comments (48, 52) landed on ADDED lines in the new file. + hunkAdded := models.DiffHunk{ + OldStartLine: 45, + OldLineCount: 6, + NewStartLine: 45, + NewLineCount: 18, + // formatSingleHunk output: + // 45 | 45 | context + // 46 | 46 | context + // | 47 | +// TextOptions controls which PII processors run... + // | 48 | +type TextOptions struct { + // | 49 | + SkipEmails bool + // | 50 | + SkipPhones bool + // | 51 | + SkipNames bool + // | 52 | + SkipAddresses bool + // ...context continues to old 50 / new 62 + Content: "@@ -45,6 +45,18 @@ type Table struct {\n" + + "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " 45 | 45 | \tColumns []Column\n" + + " 46 | 46 | }\n" + + " | 47 | +\n" + + " | 48 | +type TextOptions struct {\n" + + " | 49 | +\tSkipEmails bool\n" + + " | 50 | +\tSkipPhones bool\n" + + " | 51 | +\tSkipNames bool\n" + + " | 52 | +\tSkipAddresses bool\n" + + " | 53 | +}\n" + + " 47 | 54 | \n" + + " 48 | 55 | context\n", + } + + // Hunk 3 from the actual log: @@ -841,7 +886,6 @@ — the one with the deleted line. + hunkDeleted := models.DiffHunk{ + OldStartLine: 841, + OldLineCount: 7, + NewStartLine: 886, + NewLineCount: 6, + Content: "@@ -841,7 +886,6 @@ func (d *Deidentifier) selectBestType\n" + + "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + "841 | 886 | \n" + + "842 | 887 | \n" + + "843 | 888 | // setDefaultColumnNames generates default column names\n" + + "844 | | -func (d *Deidentifier) setDefaultColumnNames(config *slicesConfig) error {\n" + + "845 | 889 | \tif len(config.columnNames) == 0 {\n" + + "846 | 890 | \t\tconfig.columnNames = make([]string, config.numCols)\n" + + "847 | 891 | \t\tfor i := 0; i < config.numCols; i++ {\n", + } + + tests := []struct { + name string + hunk models.DiffHunk + lineNumber int + wantDeleted bool + reason string + }{ + // --- Type 3: added-line comments (LLM comments on new lines) --- + { + name: "added line 48 (TextOptions struct open brace)", + hunk: hunkAdded, + lineNumber: 48, + wantDeleted: false, + reason: "line 48 is +added in new file; OLD column is blank", + }, + { + name: "added line 52 (SkipAddresses field)", + hunk: hunkAdded, + lineNumber: 52, + wantDeleted: false, + reason: "line 52 is +added in new file; OLD column is blank", + }, + // --- Type 2: deleted-line comment (the failing case) --- + { + name: "deleted line 844 (setDefaultColumnNames func removed)", + hunk: hunkDeleted, + lineNumber: 844, + wantDeleted: true, + reason: "line 844 exists only in old file; NEW column is blank → must use 'from' in Bitbucket API", + }, + // --- Context lines (should never be flagged as deleted) --- + { + name: "context line 45 (present in both old and new)", + hunk: hunkAdded, + lineNumber: 45, + wantDeleted: false, + reason: "context line has both OLD and NEW numbers", + }, + { + name: "context line 845 (after deleted line in hunk 3)", + hunk: hunkDeleted, + lineNumber: 845, + wantDeleted: false, + reason: "line 845 is a context line after the deletion", + }, + { + name: "new-side line number 886 for context row", + hunk: hunkDeleted, + lineNumber: 886, + wantDeleted: false, + reason: "886 is the new-side number for the same context row as old 841", + }, + // --- Line outside the hunk entirely --- + { + name: "line 900 not in any hunk", + hunk: hunkDeleted, + lineNumber: 900, + wantDeleted: false, + reason: "line 900 is beyond the hunk range; default false", + }, + // --- Multiple deleted lines in one hunk: only matching one returns true --- + { + name: "first of two deleted lines", + hunk: models.DiffHunk{ + OldStartLine: 10, + OldLineCount: 4, + NewStartLine: 10, + NewLineCount: 2, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " 10 | 10 | context\n" + + " 11 | | -first removed line\n" + + " 12 | | -second removed line\n" + + " 13 | 11 | context after\n", + }, + lineNumber: 11, + wantDeleted: true, + reason: "line 11 is the first of two deleted lines", + }, + { + name: "second of two deleted lines", + hunk: models.DiffHunk{ + OldStartLine: 10, + OldLineCount: 4, + NewStartLine: 10, + NewLineCount: 2, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " 10 | 10 | context\n" + + " 11 | | -first removed line\n" + + " 12 | | -second removed line\n" + + " 13 | 11 | context after\n", + }, + lineNumber: 12, + wantDeleted: true, + reason: "line 12 is the second of two deleted lines", + }, + { + name: "context line between two deleted hunks is not deleted", + hunk: models.DiffHunk{ + OldStartLine: 10, + OldLineCount: 4, + NewStartLine: 10, + NewLineCount: 2, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " 10 | 10 | context\n" + + " 11 | | -first removed line\n" + + " 12 | | -second removed line\n" + + " 13 | 11 | context after\n", + }, + lineNumber: 10, + wantDeleted: false, + reason: "line 10 is a context line before the deletions", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := provider.lineIsDeleted(tc.lineNumber, tc.hunk) + if got != tc.wantDeleted { + t.Errorf("lineIsDeleted(%d) = %v, want %v\n reason: %s", + tc.lineNumber, got, tc.wantDeleted, tc.reason) + } + }) + } +} + +// TestLineIsDeleted_BugRegressions covers the two correctness bugs fixed in the +// new table-format parser and one additional robustness case. +// +// Bug 1 — over-broad "---" skip (original code): +// +// The old skip condition was strings.HasPrefix(line, "---"), which incorrectly +// swallowed any table row whose CONTENT column started with "---". Fixed to +// match only the exact separator row "----|-----|--------". +// +// Bug 2 — both columns fail → phantom context line at 0 (original code): +// +// When both OLD and NEW failed Atoi, parseHunkLine returned (0, 0, ..., nil). +// lineIsDeleted treated it as a context match if lineNumber == 0, producing a +// false negative. Fixed: return an error so the caller's "continue" skips it. +// +// Bonus — content containing " | " pipes must not corrupt the parse (SplitN): +// +// Because parseHunkLine uses SplitN(line, " | ", 3), a row whose CONTENT column +// contains additional " | " sequences must still be parsed correctly. +func TestLineIsDeleted_BugRegressions(t *testing.T) { + provider := &LangchainProvider{} + + tests := []struct { + name string + hunk models.DiffHunk + lineNumber int + wantDeleted bool + reason string + }{ + // ── Bug 1 regression ────────────────────────────────────────────────────── + // A deleted line whose CONTENT starts with "---" (e.g. a YAML/Markdown + // horizontal rule or an old go-style deprecation comment). + // Old code: strings.HasPrefix(" 6 | | ---", "---") == false because the + // row starts with spaces, BUT if the row happened to start with "---" directly + // (e.g. after trimming), it would be silently dropped. + // The real danger is a row like "---1 | | removed" (unlikely but possible + // with bad formatting), so we test the exact separator row is the only skip. + { + name: "bug1: deleted line with content starting with '---' is not skipped", + hunk: models.DiffHunk{ + OldStartLine: 5, + OldLineCount: 3, + NewStartLine: 5, + NewLineCount: 2, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " 5 | 5 | context line\n" + + " 6 | | ---- yaml separator removed\n" + // content starts with "---" + " 7 | 6 | context after\n", + }, + lineNumber: 6, + wantDeleted: true, + reason: "OLD=6, NEW=blank → deleted; the '---' in CONTENT must not cause " + + "the row to be skipped (old broad HasPrefix check was the bug)", + }, + { + name: "bug1: exact separator row ----|-----|-------- is still skipped", + hunk: models.DiffHunk{ + OldStartLine: 5, + OldLineCount: 2, + NewStartLine: 5, + NewLineCount: 2, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + // must be skipped, not parsed as data + " 5 | 5 | context\n" + + " 6 | 6 | context\n", + }, + lineNumber: 0, // no row should produce a match at 0 + wantDeleted: false, + reason: "separator row must be skipped; it must not be parsed as a data row " + + "that could match line 0", + }, + + // ── Bug 2 regression ────────────────────────────────────────────────────── + // A row where BOTH OLD and NEW columns are non-numeric (completely garbled). + // Old code: parseHunkLine returned (0, 0, content, false, false, nil), so + // lineIsDeleted treated it as a context line matching oldNum==0 / newNum==0. + // If lineNumber == 0 was ever queried, it would return false (wrong match). + // New code: returns an error → caller's "continue" skips the row cleanly. + { + name: "bug2: garbled row with non-numeric OLD and NEW does not match line 0", + hunk: models.DiffHunk{ + OldStartLine: 1, + OldLineCount: 2, + NewStartLine: 1, + NewLineCount: 2, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " x | y | garbage row both non-numeric\n" + // both columns fail Atoi + " 1 | 1 | real context\n", + }, + lineNumber: 0, // should never match because 0 is not a real line number + wantDeleted: false, + reason: "unparseable row (both OLD and NEW non-numeric) must not produce a " + + "phantom context match at line 0 — it must be skipped via error return", + }, + + // ── Bonus: pipe characters inside content column ─────────────────────────── + // parseHunkLine uses SplitN(line, " | ", 3), so extra " | " in CONTENT is safe. + { + name: "bonus: deleted line whose content contains ' | ' pipe sequences", + hunk: models.DiffHunk{ + OldStartLine: 20, + OldLineCount: 2, + NewStartLine: 20, + NewLineCount: 1, + Content: "OLD | NEW | CONTENT\n" + + "----|-----|--------\n" + + " 20 | 20 | context\n" + + " 21 | | -val := a | b | c\n", // content has " | " in it + }, + lineNumber: 21, + wantDeleted: true, + reason: "SplitN(..., 3) limits splits to 3 parts, so extra ' | ' in the " + + "CONTENT column does not corrupt OLD/NEW number parsing", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := provider.lineIsDeleted(tc.lineNumber, tc.hunk) + if got != tc.wantDeleted { + t.Errorf("lineIsDeleted(%d) = %v, want %v\n reason: %s", + tc.lineNumber, got, tc.wantDeleted, tc.reason) + } + }) + } +} + +// TestLineIsDeleted_FormatContract documents the data-flow guarantee that +// lineIsDeleted always receives pre-formatted table content, never raw unified diff. +// +// Data flow (confirmed in provider.go): +// +// ReviewCodeWithBatching / ReviewCodeWithBatchingV2 +// └─ formatHunkWithLineNumbers(hunk) ← converts +/- → table, in-place +// └─ diff.Hunks[j].Content = formatted ← same slice passed downstream +// └─ parseResponseWithRepair(diffs) ← lineIsDeleted reads this +// +// The test below documents the known silent-failure mode: if raw unified diff +// content somehow bypassed the formatting step, lineIsDeleted would return false +// for everything (all rows fail the " | " split, all are skipped). +// This is NOT a bug in lineIsDeleted — it is the caller's responsibility to +// ensure formatHunkWithLineNumbers has run first. +func TestLineIsDeleted_FormatContract(t *testing.T) { + provider := &LangchainProvider{} + + // Raw unified diff content — exactly what the OLD lineIsDeleted used to receive, + // and what the NEW one should never see at runtime. + rawUnifiedDiff := models.DiffHunk{ + OldStartLine: 841, + OldLineCount: 7, + NewStartLine: 886, + NewLineCount: 6, + Content: "@@ -841,7 +886,6 @@ func (d *Deidentifier) selectBestType\n" + + " \n" + + " \n" + + " // setDefaultColumnNames generates default column names\n" + + "-func (d *Deidentifier) setDefaultColumnNames(config *slicesConfig) error {\n" + + " \tif len(config.columnNames) == 0 {\n" + + " \t\tconfig.columnNames = make([]string, config.numCols)\n" + + " \t\tfor i := 0; i < config.numCols; i++ {\n", + } + + // With raw unified diff, every data row fails len(parts) != 3 (no " | "), + // so all rows are skipped and the result is always false. + // This is the silent failure mode — not a crash, but incorrect. + // At runtime this cannot happen because formatHunkWithLineNumbers always runs first. + got := provider.lineIsDeleted(844, rawUnifiedDiff) + + // The point of this test is documentation: confirm the current silent-skip + // behavior is stable, so a future change that accidentally makes the parser + // handle raw diffs (and potentially return wrong results) is flagged. + if got { + t.Errorf("lineIsDeleted(844, rawUnifiedDiff) = true; "+ + "raw unified diff hitting the table parser should silently return false, "+ + "not a correct result — check whether formatHunkWithLineNumbers was bypassed") + } +} diff --git a/internal/ai/langchain/provider.go b/internal/ai/langchain/provider.go index 441fc2c1..0a3c3965 100644 --- a/internal/ai/langchain/provider.go +++ b/internal/ai/langchain/provider.go @@ -1415,34 +1415,69 @@ func (p *LangchainProvider) lineInHunk(lineNumber int, hunk models.DiffHunk) boo // lineIsDeleted analyzes hunk content to determine if a line is deleted func (p *LangchainProvider) lineIsDeleted(lineNumber int, hunk models.DiffHunk) bool { lines := strings.Split(hunk.Content, "\n") - oldLine := hunk.OldStartLine - newLine := hunk.NewStartLine for _, line := range lines { - if strings.HasPrefix(line, "@@") { - continue // Skip hunk header + // Skip table header rows, blank lines, raw hunk headers, and the + // fixed separator row "----|-----|--------". + // NOTE: do NOT use strings.HasPrefix(line, "---") here — that would + // incorrectly swallow table rows whose CONTENT column starts with + // "---" (e.g. deleted lines containing "---old-value"). + if strings.HasPrefix(line, "OLD") || line == "----|-----|--------" || strings.HasPrefix(line, "@@") || line == "" { + continue + } + + oldNum, newNum, _, isDeleted, isAdded, err := parseHunkLine(line) + if err != nil { + continue } - if strings.HasPrefix(line, "-") { - if oldLine == lineNumber { + if isDeleted { + if oldNum == lineNumber { return true } - oldLine++ - } else if strings.HasPrefix(line, "+") { - newLine++ + } else if isAdded { + if newNum == lineNumber { + return false + } } else { // Context line - if oldLine == lineNumber || newLine == lineNumber { - return false // Context lines are not deleted + if oldNum == lineNumber || newNum == lineNumber { + return false } - oldLine++ - newLine++ } } return false } +// parseHunkLine parses a formatted table row "OLD | NEW | CONTENT" +func parseHunkLine(line string) (oldNum int, newNum int, content string, isDeleted bool, isAdded bool, err error) { + parts := strings.SplitN(line, " | ", 3) + if len(parts) != 3 { + return 0, 0, "", false, false, fmt.Errorf("invalid table row format") + } + + oldStr := strings.TrimSpace(parts[0]) + newStr := strings.TrimSpace(parts[1]) + content = parts[2] + + var oldErr, newErr error + oldNum, oldErr = strconv.Atoi(oldStr) + newNum, newErr = strconv.Atoi(newStr) + + if oldErr == nil && newErr != nil { + isDeleted = true + } else if oldErr != nil && newErr == nil { + isAdded = true + } else if oldErr != nil && newErr != nil { + // Neither column is a valid integer — this is not a recognisable row. + // Return an error so the caller skips it rather than treating it as a + // phantom context line at position 0. + return 0, 0, "", false, false, fmt.Errorf("unparseable table row: both OLD=%q and NEW=%q are non-numeric", oldStr, newStr) + } + + return oldNum, newNum, content, isDeleted, isAdded, nil +} // Helper functions for logging func truncateString(s string, maxLen int) string { if len(s) <= maxLen { @@ -1559,14 +1594,14 @@ func (p *LangchainProvider) logLLMErrorDetails(err error, batchID string) { baseMsg := fmt.Sprintf("%s (cause %d): type=%T msg=%v", prefix, depth, current, current) fmt.Printf("[LANGCHAIN ERROR DETAIL] %s\n", baseMsg) if p.logger != nil { - p.logger.Log(baseMsg) + p.logger.Log("%s", baseMsg) } if extra := describeStructuredError(current); extra != "" { detailMsg := fmt.Sprintf("%s (cause %d) detail: %s", prefix, depth, extra) fmt.Printf("[LANGCHAIN ERROR DETAIL] %s\n", detailMsg) if p.logger != nil { - p.logger.Log(detailMsg) + p.logger.Log("%s", detailMsg) } } } diff --git a/internal/api/bitbucket_profile.go b/internal/api/bitbucket_profile.go index bccd1b77..2962f2ae 100644 --- a/internal/api/bitbucket_profile.go +++ b/internal/api/bitbucket_profile.go @@ -1,10 +1,13 @@ package api import ( + "context" "encoding/json" "fmt" "io" - "net/http" + "time" + + networkbitbucket "github.com/livereview/network/providers/bitbucket" ) // BitbucketProfile represents the user profile info fetched from Bitbucket @@ -23,33 +26,20 @@ type BitbucketProfile struct { // FetchBitbucketProfile fetches the user profile from Bitbucket using Atlassian API token func FetchBitbucketProfile(email, apiToken string) (*BitbucketProfile, error) { - // First, let's try a simpler endpoint to test authentication - url := "https://api.bitbucket.org/2.0/user" - client := &http.Client{} - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request - please check the request format") - } - - // Use Basic Auth with email and Atlassian API token - req.SetBasicAuth(email, apiToken) - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "LiveReview/1.0") - - resp, err := client.Do(req) + const bitbucketBaseAPI = "https://api.bitbucket.org" + client := networkbitbucket.NewHTTPClient(15 * time.Second) + resp, err := networkbitbucket.FetchUserProfile(context.Background(), client, bitbucketBaseAPI, email, apiToken) if err != nil { return nil, fmt.Errorf("cannot connect to Bitbucket - please verify your internet connection") } defer resp.Body.Close() - // Read response body for debugging body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body") } if resp.StatusCode != 200 { - // Log the actual response for debugging fmt.Printf("Bitbucket API response status: %d\n", resp.StatusCode) fmt.Printf("Bitbucket API response body: %s\n", string(body)) @@ -76,25 +66,14 @@ func FetchBitbucketProfile(email, apiToken string) (*BitbucketProfile, error) { // ValidateBitbucketToken validates a Bitbucket API token by making a simple API call func ValidateBitbucketToken(email, apiToken string) error { - // Use the same endpoint as profile fetching for consistency - url := "https://api.bitbucket.org/2.0/user" - client := &http.Client{} - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return fmt.Errorf("failed to create validation request") - } - - req.SetBasicAuth(email, apiToken) - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "LiveReview/1.0") - - resp, err := client.Do(req) + const bitbucketBaseAPI = "https://api.bitbucket.org" + client := networkbitbucket.NewHTTPClient(15 * time.Second) + resp, err := networkbitbucket.FetchUserProfile(context.Background(), client, bitbucketBaseAPI, email, apiToken) if err != nil { return fmt.Errorf("cannot connect to Bitbucket API") } defer resp.Body.Close() - // Read response body for debugging body, err := io.ReadAll(resp.Body) if err != nil { fmt.Printf("Failed to read validation response body: %v\n", err) diff --git a/internal/api/reviews_api.go b/internal/api/reviews_api.go index ff6a861d..4783b1d6 100644 --- a/internal/api/reviews_api.go +++ b/internal/api/reviews_api.go @@ -582,6 +582,8 @@ func (s *Server) buildReviewRequest( providerConfigMap["email"] = email } } + // Pass the full PR URL so the factory can parse workspace/repo from it + providerConfigMap["repo_url"] = requestURL } else if strings.HasPrefix(token.Provider, "gitea") { pat, user, pass := decodePATPayload(token.PatToken) if pat == "" { diff --git a/internal/api/unified_processor_v2.go b/internal/api/unified_processor_v2.go index 00899d06..48f42993 100644 --- a/internal/api/unified_processor_v2.go +++ b/internal/api/unified_processor_v2.go @@ -506,8 +506,9 @@ func (p *UnifiedProcessorV2Impl) buildContextualResponseWithLearningV2(ctx conte llmResponse, learning, usage, err := p.generateLLMResponseWithLearning(ctx, prompt, event, orgID) if err != nil { - log.Printf("[ERROR] LLM generation failed: %v - cannot provide response", err) - return fmt.Sprintf("I'm sorry, I'm unable to generate a response right now. Please try again later. (Error: %v)", err), nil, nil + log.Printf("[ERROR] LLM generation failed: %v", err) + // Return generic error to user to avoid exposing internal API errors + return "⚠️ Failed to generate AI response\nThis issue has been logged and will be investigated.", nil, nil } return llmResponse, learning, usage @@ -1366,7 +1367,7 @@ func (p *UnifiedProcessorV2Impl) buildGitLabArtifactFromEvent(ctx context.Contex // Look up GitLab PAT from integration_tokens table using base URL and org_id query := `SELECT pat_token FROM integration_tokens - WHERE provider IN ('gitlab', 'GitLab', 'gitlab-self-hosted') + WHERE provider IN ('gitlab', 'GitLab', 'gitlab-self-hosted', 'gitlab-com') AND RTRIM(provider_url, '/') = $1 AND org_id = $2 LIMIT 1` @@ -1505,22 +1506,32 @@ func (p *UnifiedProcessorV2Impl) buildBitbucketArtifactFromEvent(ctx context.Con log.Printf("[DEBUG] Constructed Bitbucket PR URL: %s (org_id=%d)", prURL, orgID) // Look up Bitbucket credentials from integration_tokens table, filtered by org_id - query := `SELECT pat_token FROM integration_tokens + // Also fetch metadata to extract the email associated with this token + query := `SELECT pat_token, COALESCE(metadata, '{}') FROM integration_tokens WHERE provider IN ('bitbucket', 'Bitbucket') AND org_id = $1 LIMIT 1` - var patToken string - err := p.server.DB().QueryRow(query, orgID).Scan(&patToken) + var patToken, metadataJSON string + err := p.server.DB().QueryRow(query, orgID).Scan(&patToken, &metadataJSON) if err != nil { return nil, fmt.Errorf("failed to find Bitbucket PAT for org %d: %w", orgID, err) } log.Printf("[DEBUG] Found Bitbucket PAT for org %d", orgID) - // Bitbucket provider needs email - use default for bot - // In production, this could come from metadata or config - botEmail := "livereviewbot@gmail.com" + // Extract email from metadata (set during token registration) — required for Basic Auth + var tokenMetadata map[string]interface{} + if metadataJSON != "" && metadataJSON != "{}" { + if jsonErr := json.Unmarshal([]byte(metadataJSON), &tokenMetadata); jsonErr != nil { + log.Printf("[WARN] Failed to parse Bitbucket token metadata for org %d: %v", orgID, jsonErr) + } + } + botEmail, _ := tokenMetadata["email"].(string) + if botEmail == "" { + return nil, fmt.Errorf("Bitbucket token for org %d is missing 'email' in metadata; re-connect the integration", orgID) + } + log.Printf("[DEBUG] Using Bitbucket email from token metadata: %s", botEmail) // Create Bitbucket provider (following cli.go pattern) provider, err := bitbucketmentions.NewBitbucketProvider(patToken, botEmail, prURL) @@ -1533,7 +1544,7 @@ func (p *UnifiedProcessorV2Impl) buildBitbucketArtifactFromEvent(ctx context.Con mrModel.EnableArtifactWriting = false // Don't write to disk // Build Bitbucket artifact (following cli.go pattern) - artifact, err := mrModel.BuildBitbucketArtifact(provider, prID, prURL, "") + artifact, err := mrModel.BuildBitbucketArtifact(ctx, provider, prID, prURL, "") if err != nil { return nil, fmt.Errorf("failed to build Bitbucket artifact: %w", err) } @@ -1580,8 +1591,21 @@ func (p *UnifiedProcessorV2Impl) buildGiteaArtifactFromEvent(ctx context.Context patchURL = strings.TrimSpace(rawPatchURL) } } - if patchURL == "" && strings.TrimSpace(event.MergeRequest.WebURL) != "" { - patchURL = strings.TrimSpace(event.MergeRequest.WebURL) + ".patch" + if patchURL == "" { + // Use provider URL and metadata to construct API patch URL reliably. + // We avoid brittle WebURL path manipulation by using known metadata. + baseURL := strings.TrimRight(token.ProviderURL, "/") + repoFullName := event.Repository.FullName + prNumber := event.MergeRequest.Number + + if baseURL != "" && repoFullName != "" && prNumber > 0 { + patchURL = fmt.Sprintf("%s/api/v1/repos/%s/pulls/%d.patch", + baseURL, repoFullName, prNumber) + } else if webURL := strings.TrimSpace(event.MergeRequest.WebURL); webURL != "" { + // Fallback: Gitea supports appending .patch to the UI URL. + // This is a safe last-resort if API-specific metadata is incomplete. + patchURL = strings.TrimRight(webURL, "/") + ".patch" + } } if patchURL == "" { return nil, fmt.Errorf("missing gitea patch URL") diff --git a/internal/provider_input/bitbucket/bitbucket_provider_v2.go b/internal/provider_input/bitbucket/bitbucket_provider_v2.go index 5501a152..05d8ad53 100644 --- a/internal/provider_input/bitbucket/bitbucket_provider_v2.go +++ b/internal/provider_input/bitbucket/bitbucket_provider_v2.go @@ -572,7 +572,12 @@ func (p *BitbucketV2Provider) PostCommentReply(event *UnifiedWebhookEventV2, con return fmt.Errorf("bitbucket email missing in integration token metadata; cannot authenticate") } - return p.output.PostCommentReply(workspace, repository, fmt.Sprintf("%d", prNumber), event.Comment.InReplyToID, content, email, token.PatToken) + replyTo := event.Comment.DiscussionID + if replyTo == nil || *replyTo == "" { + replyTo = &event.Comment.ID + } + + return p.output.PostCommentReply(workspace, repository, fmt.Sprintf("%d", prNumber), replyTo, content, email, token.PatToken) } // PostEmojiReaction posts an emoji reaction diff --git a/internal/providers/bitbucket/bitbucket.go b/internal/providers/bitbucket/bitbucket.go index 5a395584..e60eb01a 100644 --- a/internal/providers/bitbucket/bitbucket.go +++ b/internal/providers/bitbucket/bitbucket.go @@ -3,7 +3,6 @@ package bitbucket import ( "bytes" "context" - "encoding/base64" "encoding/json" "fmt" "io" @@ -16,6 +15,7 @@ import ( "time" "github.com/livereview/internal/providers" + networkbitbucket "github.com/livereview/network/providers/bitbucket" "github.com/livereview/pkg/models" "golang.org/x/time/rate" ) @@ -93,11 +93,24 @@ func NewBitbucketProvider(token, email, repoURL string) (*BitbucketProvider, err repoURL: repoURL, workspace: workspace, repoSlug: repoSlug, - httpClient: &http.Client{Timeout: 10 * time.Second}, + httpClient: networkbitbucket.NewHTTPClient(10 * time.Second), RateLimiter: rate.NewLimiter(rate.Every(1*time.Second), 5), // 5 requests per second }, nil } +var mrIDRegex = regexp.MustCompile(`^(?:https?://[^/]+/)?([^/]+)/([^/]+)/(?:pull-requests/)?(\d+)(?:/.*)?$`) + +// extractMRIDComponents explicitly parses an mrID/prID using a regex +// to reliably extract the workspace, repository, and pull request number. +func extractMRIDComponents(id string) (workspace, repo, prNum string, err error) { + id = strings.TrimSpace(id) + matches := mrIDRegex.FindStringSubmatch(id) + if len(matches) != 4 { + return "", "", "", fmt.Errorf("invalid Bitbucket PR identifier format: expected 'workspace/repo/number' or a valid URL, got '%s'", id) + } + return matches[1], matches[2], matches[3], nil +} + func ParseBitbucketURL(urlStr string) (string, string, string, error) { if urlStr == "" { return "", "", "", fmt.Errorf("repository URL is empty") @@ -134,17 +147,16 @@ func (p *BitbucketProvider) GetMergeRequestDetails(ctx context.Context, prURL st apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s", p.workspace, p.repoSlug, prID) log.Printf("[DEBUG] BitbucketProvider: API URL: %s", apiURL) - req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + req, err := networkbitbucket.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } // Set Basic Auth header - auth := base64.StdEncoding.EncodeToString([]byte(p.email + ":" + p.token)) - req.Header.Set("Authorization", "Basic "+auth) + req.SetBasicAuth(p.email, p.token) req.Header.Set("Accept", "application/json") - resp, err := p.httpClient.Do(req) + resp, err := networkbitbucket.Do(p.httpClient, req) if err != nil { return nil, fmt.Errorf("failed to fetch PR details: %w", err) } @@ -225,32 +237,27 @@ func (p *BitbucketProvider) GetMergeRequestDetails(ctx context.Context, prURL st func (p *BitbucketProvider) GetMergeRequestChanges(ctx context.Context, prID string) ([]*models.CodeDiff, error) { log.Printf("[DEBUG] BitbucketProvider.GetMergeRequestChanges called with prID: %s", prID) - // Parse prID which should be in format "workspace/repo/prNumber" - parts := strings.Split(prID, "/") - if len(parts) != 3 { - return nil, fmt.Errorf("invalid Bitbucket PR ID format: expected 'workspace/repo/number', got '%s'", prID) + // Use regex-based parser for robustness + workspace, repo, prNumber, err := extractMRIDComponents(prID) + if err != nil { + return nil, err } - workspace := parts[0] - repo := parts[1] - prNumber := parts[2] - // Bitbucket API v2.0 endpoint for pull request diff apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/diff", workspace, repo, prNumber) log.Printf("[DEBUG] BitbucketProvider: Diff API URL: %s", apiURL) - req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + req, err := networkbitbucket.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } // Set Basic Auth header - auth := base64.StdEncoding.EncodeToString([]byte(p.email + ":" + p.token)) - req.Header.Set("Authorization", "Basic "+auth) + req.SetBasicAuth(p.email, p.token) req.Header.Set("Accept", "text/plain") - resp, err := http.DefaultClient.Do(req) + resp, err := networkbitbucket.Do(http.DefaultClient, req) if err != nil { return nil, fmt.Errorf("failed to fetch PR diff: %w", err) } @@ -288,32 +295,27 @@ func (p *BitbucketProvider) GetMergeRequestChanges(ctx context.Context, prID str func (p *BitbucketProvider) GetMergeRequestChangesAsText(ctx context.Context, prID string) (string, error) { log.Printf("[DEBUG] BitbucketProvider.GetMergeRequestChangesAsText called with prID: %s", prID) - // Parse prID which should be in format "workspace/repo/prNumber" - parts := strings.Split(prID, "/") - if len(parts) != 3 { - return "", fmt.Errorf("invalid Bitbucket PR ID format: expected 'workspace/repo/number', got '%s'", prID) + // Use regex-based parser for robustness + workspace, repo, prNumber, err := extractMRIDComponents(prID) + if err != nil { + return "", err } - workspace := parts[0] - repo := parts[1] - prNumber := parts[2] - // Bitbucket API v2.0 endpoint for pull request diff apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/diff", workspace, repo, prNumber) log.Printf("[DEBUG] BitbucketProvider: Diff API URL: %s", apiURL) - req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + req, err := networkbitbucket.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } // Set Basic Auth header - auth := base64.StdEncoding.EncodeToString([]byte(p.email + ":" + p.token)) - req.Header.Set("Authorization", "Basic "+auth) + req.SetBasicAuth(p.email, p.token) req.Header.Set("Accept", "text/plain") - resp, err := http.DefaultClient.Do(req) + resp, err := networkbitbucket.Do(http.DefaultClient, req) if err != nil { return "", fmt.Errorf("failed to fetch PR diff: %w", err) } @@ -333,9 +335,9 @@ func (p *BitbucketProvider) GetMergeRequestChangesAsText(ctx context.Context, pr return string(diffContent), nil } -func (p *BitbucketProvider) GetPullRequestCommits(prID string) ([]BitbucketCommit, error) { +func (p *BitbucketProvider) GetPullRequestCommits(ctx context.Context, prID string) ([]BitbucketCommit, error) { apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/commits", p.workspace, p.repoSlug, prID) - body, err := p.doRequest(apiURL, "GET", nil) + body, err := p.doRequest(ctx, apiURL, "GET", nil) if err != nil { return nil, fmt.Errorf("failed to fetch PR commits: %w", err) } @@ -351,9 +353,9 @@ func (p *BitbucketProvider) GetPullRequestCommits(prID string) ([]BitbucketCommi return response.Values, nil } -func (p *BitbucketProvider) GetPullRequestComments(prID string) ([]BitbucketComment, error) { +func (p *BitbucketProvider) GetPullRequestComments(ctx context.Context, prID string) ([]BitbucketComment, error) { apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/comments?sort=-created_on", p.workspace, p.repoSlug, prID) - body, err := p.doRequest(apiURL, "GET", nil) + body, err := p.doRequest(ctx, apiURL, "GET", nil) if err != nil { return nil, fmt.Errorf("failed to fetch PR comments: %w", err) } @@ -369,9 +371,9 @@ func (p *BitbucketProvider) GetPullRequestComments(prID string) ([]BitbucketComm return response.Values, nil } -func (p *BitbucketProvider) GetPullRequestDiff(prID string) (string, error) { +func (p *BitbucketProvider) GetPullRequestDiff(ctx context.Context, prID string) (string, error) { apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/diff", p.workspace, p.repoSlug, prID) - body, err := p.doRequest(apiURL, "GET", nil) + body, err := p.doRequest(ctx, apiURL, "GET", nil) if err != nil { return "", fmt.Errorf("failed to fetch PR diff: %w", err) } @@ -379,7 +381,7 @@ func (p *BitbucketProvider) GetPullRequestDiff(prID string) (string, error) { return string(body), nil } -func (p *BitbucketProvider) doRequest(apiURL, method string, payload interface{}) ([]byte, error) { +func (p *BitbucketProvider) doRequest(ctx context.Context, apiURL, method string, payload interface{}) ([]byte, error) { var body io.Reader if payload != nil { data, err := json.Marshal(payload) @@ -389,18 +391,17 @@ func (p *BitbucketProvider) doRequest(apiURL, method string, payload interface{} body = bytes.NewReader(data) } - req, err := http.NewRequest(method, apiURL, body) + req, err := networkbitbucket.NewRequestWithContext(ctx, method, apiURL, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } // Set Basic Auth header - auth := base64.StdEncoding.EncodeToString([]byte(p.email + ":" + p.token)) - req.Header.Set("Authorization", "Basic "+auth) + req.SetBasicAuth(p.email, p.token) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") - resp, err := p.httpClient.Do(req) + resp, err := networkbitbucket.Do(p.httpClient, req) if err != nil { return nil, fmt.Errorf("failed to execute request: %w", err) } @@ -572,11 +573,121 @@ func (p *BitbucketProvider) Name() string { } func (p *BitbucketProvider) PostComment(ctx context.Context, mrID string, comment *models.ReviewComment) error { - return fmt.Errorf("not implemented") + log.Printf("[DEBUG] BitbucketProvider.PostComment called with mrID: '%s', FilePath: '%s', Line: %d", mrID, comment.FilePath, comment.Line) + + workspace, repo, prNumber, err := extractMRIDComponents(mrID) + if err != nil { + return err + } + + if comment.FilePath != "" && comment.Line > 0 { + return p.postLineComment(ctx, workspace, repo, prNumber, comment) + } + return p.postGeneralComment(ctx, workspace, repo, prNumber, comment) +} + +// formatBitbucketComment formats a comment for Bitbucket, including severity and suggestions. +func formatBitbucketComment(comment *models.ReviewComment) string { + body := comment.Content + if comment.Severity != "" { + body = fmt.Sprintf("**Severity: %s**\n\n%s", comment.Severity, body) + } + if len(comment.Suggestions) > 0 { + body += "\n\n**Suggestions:**\n" + for i, s := range comment.Suggestions { + body += fmt.Sprintf("%d. %s\n", i+1, s) + } + } + return body +} + +func (p *BitbucketProvider) postGeneralComment(ctx context.Context, workspace, repo, prNumber string, comment *models.ReviewComment) error { + apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/comments", workspace, repo, prNumber) + + payload := map[string]interface{}{ + "content": map[string]string{ + "raw": formatBitbucketComment(comment), + }, + } + + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal comment payload: %w", err) + } + + resp, err := networkbitbucket.PostCommentAPI(ctx, p.httpClient, apiURL, p.email, p.token, data) + if err != nil { + return fmt.Errorf("failed to post general comment: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Bitbucket general comment failed: %s, response: %s", resp.Status, string(body)) + } + + log.Printf("[DEBUG] BitbucketProvider: Successfully posted general comment on PR %s/%s/%s", workspace, repo, prNumber) + return nil +} + +func (p *BitbucketProvider) postLineComment(ctx context.Context, workspace, repo, prNumber string, comment *models.ReviewComment) error { + apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/comments", workspace, repo, prNumber) + + // Bitbucket inline comments use the "inline" object with "path" and "to" (new line) + // or "from" (old line) for deleted lines. + inlinePayload := map[string]interface{}{ + "path": comment.FilePath, + "to": comment.Line, + } + if comment.IsDeletedLine { + // For deleted lines, use "from" instead of "to" + inlinePayload = map[string]interface{}{ + "path": comment.FilePath, + "from": comment.Line, + } + } + + payload := map[string]interface{}{ + "content": map[string]string{ + "raw": formatBitbucketComment(comment), + }, + "inline": inlinePayload, + } + + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal inline comment payload: %w", err) + } + + resp, err := networkbitbucket.PostCommentAPI(ctx, p.httpClient, apiURL, p.email, p.token, data) + if err != nil { + return fmt.Errorf("failed to post inline comment: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + // If the line is not part of the diff, Bitbucket returns 400/422. + // Fall back to a general comment rather than failing the whole review. + if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnprocessableEntity { + log.Printf("[WARN] BitbucketProvider: Inline comment rejected for %s:%d (%s) — falling back to general comment. Response: %s", + comment.FilePath, comment.Line, resp.Status, string(body)) + return p.postGeneralComment(ctx, workspace, repo, prNumber, comment) + } + return fmt.Errorf("Bitbucket inline comment failed: %s, response: %s", resp.Status, string(body)) + } + + log.Printf("[DEBUG] BitbucketProvider: Successfully posted inline comment on %s:%d", comment.FilePath, comment.Line) + return nil } func (p *BitbucketProvider) PostComments(ctx context.Context, mrID string, comments []*models.ReviewComment) error { - return fmt.Errorf("not implemented") + for _, comment := range comments { + if err := p.PostComment(ctx, mrID, comment); err != nil { + return err + } + } + return nil } func (p *BitbucketProvider) Configure(config map[string]interface{}) error { diff --git a/internal/review/factories.go b/internal/review/factories.go index e6159378..b01b3391 100644 --- a/internal/review/factories.go +++ b/internal/review/factories.go @@ -53,8 +53,15 @@ func (f *StandardProviderFactory) CreateProvider(ctx context.Context, config Pro log.Printf("[DEBUG] Creating Bitbucket provider") apiToken, _ := config.Config["pat_token"].(string) email, _ := config.Config["email"].(string) - log.Printf("[DEBUG] Bitbucket token exists: %v, email: %s", len(apiToken) > 0, email) - provider, err := bitbucket.NewBitbucketProvider(apiToken, email, "") + // Prefer the explicit repo_url from the config map (PR URL set by the API + // layer), fall back to the provider base URL. An empty string would cause + // ParseBitbucketURL to fail immediately. + repoURL, _ := config.Config["repo_url"].(string) + if repoURL == "" { + return nil, fmt.Errorf("failed to create bitbucket provider: repo_url is required but was not provided") + } + log.Printf("[DEBUG] Bitbucket token exists: %v, email: %s, repoURL: %s", len(apiToken) > 0, email, repoURL) + provider, err := bitbucket.NewBitbucketProvider(apiToken, email, repoURL) if err != nil { return nil, fmt.Errorf("failed to create bitbucket provider: %w", err) } diff --git a/network/network_status.md b/network/network_status.md index c2ad788f..ec7b95e4 100644 --- a/network/network_status.md +++ b/network/network_status.md @@ -41,9 +41,11 @@ Latest milestone batch note (MF-051, MF-059, MF-073, MF-074, MF-076, MF-083, MF- | providersgitlab.NewRequestWithContext | moved | [NewRequestWithContext](providers/gitlab/http_client_ops.go#L29) | | providersgitlab.Do | moved | [Do](providers/gitlab/http_client_ops.go#L37) | | providersgitlab.ParseURL | moved | [ParseURL](providers/gitlab/http_client_ops.go#L47) | -| providersbitbucket.NewHTTPClient | moved | [NewHTTPClient](providers/bitbucket/http_client_ops.go#L11) | -| providersbitbucket.NewRequestWithContext | moved | [NewRequestWithContext](providers/bitbucket/http_client_ops.go#L18) | -| providersbitbucket.Do | moved | [Do](providers/bitbucket/http_client_ops.go#L27) | +| providersbitbucket.NewHTTPClient | moved | [NewHTTPClient](providers/bitbucket/http_client_ops.go#L13) | +| providersbitbucket.NewRequestWithContext | moved | [NewRequestWithContext](providers/bitbucket/http_client_ops.go#L20) | +| providersbitbucket.Do | moved | [Do](providers/bitbucket/http_client_ops.go#L29) | +| providersbitbucket.PostCommentAPI | added | [PostCommentAPI](providers/bitbucket/http_client_ops.go#L40) | +| providersbitbucket.FetchUserProfile | added | [FetchUserProfile](providers/bitbucket/http_client_ops.go#L56) | | aiconnectors.NewHTTPClient | moved | [NewHTTPClient](aiconnectors/http_client_ops.go#L11) | | aiconnectors.NewRequestWithContext | moved | [NewRequestWithContext](aiconnectors/http_client_ops.go#L18) | | aiconnectors.Do | moved | [Do](aiconnectors/http_client_ops.go#L26) | diff --git a/network/providers/bitbucket/http_client_ops.go b/network/providers/bitbucket/http_client_ops.go index 9ba760a6..faf34341 100644 --- a/network/providers/bitbucket/http_client_ops.go +++ b/network/providers/bitbucket/http_client_ops.go @@ -1,10 +1,12 @@ package bitbucket import ( + "bytes" "context" "fmt" "io" "net/http" + "strings" "time" ) @@ -33,3 +35,37 @@ func Do(client *http.Client, req *http.Request) (*http.Response, error) { } return client.Do(req) } + +// PostCommentAPI handles the exact HTTP execution and authorization for posting Bitbucket comments. +func PostCommentAPI(ctx context.Context, client *http.Client, apiURL, email, token string, payload []byte) (*http.Response, error) { + importBytes := bytes.NewReader(payload) + req, err := NewRequestWithContext(ctx, "POST", apiURL, importBytes) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.SetBasicAuth(email, token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + return Do(client, req) +} + +// FetchUserProfile fetches the authenticated user's profile from Bitbucket. +// Callers must close resp.Body when err is nil. +func FetchUserProfile(ctx context.Context, client *http.Client, baseURL, email, token string) (*http.Response, error) { + if baseURL == "" { + baseURL = "https://api.bitbucket.org" + } + baseURL = strings.TrimRight(baseURL, "/") + apiURL := fmt.Sprintf("%s/2.0/user", baseURL) + + req, err := NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create user profile request: %w", err) + } + req.SetBasicAuth(email, token) + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "LiveReview/1.0") + return Do(client, req) +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 8b6efbef..8a66e2f1 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -69,7 +69,7 @@ "jest-environment-jsdom": "^29.7.0", "lint-staged": "^16.4.0", "mini-css-extract-plugin": "^2.9.0", - "postcss": "8.4", + "postcss": "^8.5.10", "postcss-loader": "7.3", "postcss-preset-env": "8.4", "prettier": "^2.7.1", @@ -3048,37 +3048,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@inversifyjs/common": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.3.3.tgz", - "integrity": "sha512-ZH0wrgaJwIo3s9gMCDM2wZoxqrJ6gB97jWXncROfYdqZJv8f3EkqT57faZqN5OTeHWgtziQ6F6g3L8rCvGceCw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@inversifyjs/core": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.4.tgz", - "integrity": "sha512-gCCmA4BdbHEFwvVZ2elWgHuXZWk6AOu/1frxsS+2fWhjEk2c/IhtypLo5ytSUie1BCiT6i9qnEo4bruBomQsAA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@inversifyjs/common": "1.3.3", - "@inversifyjs/reflect-metadata-utils": "0.2.3" - } - }, - "node_modules/@inversifyjs/reflect-metadata-utils": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.3.tgz", - "integrity": "sha512-d3D0o9TeSlvaGM2I24wcNw/Aj3rc4OYvHXOKDC09YEph5fMMiKd6fq1VTQd9tOkDNWvVbw+cnt45Wy9P/t5Lvw==", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "reflect-metadata": "0.2.2" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3196,95 +3165,6 @@ "node": ">=8" } }, - "node_modules/@javascript-obfuscator/escodegen": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.3.1.tgz", - "integrity": "sha512-Z0HEAVwwafOume+6LFXirAVZeuEMKWuPzpFbQhCEU9++BMz0IwEa9bmedJ+rMn/IlXRBID9j3gQ0XYAa6jM10g==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@javascript-obfuscator/estraverse": "^5.3.0", - "esprima": "^4.0.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/@javascript-obfuscator/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/@javascript-obfuscator/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/@javascript-obfuscator/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/@javascript-obfuscator/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/@javascript-obfuscator/estraverse": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@javascript-obfuscator/estraverse/-/estraverse-5.4.0.tgz", - "integrity": "sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@jest/console": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", @@ -4790,27 +4670,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -4890,14 +4749,6 @@ "node": ">= 10" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5230,7 +5081,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5393,14 +5244,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -5671,24 +5514,6 @@ "dev": true, "license": "ISC" }, - "node_modules/@vercel/blob": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.3.1.tgz", - "integrity": "sha512-6f9oWC+DbWxIgBLOdqjjn2/REpFrPDB7y5B5HA1ptYkzZaBgL6E34kWrptJvJ7teApJdbAs3I1a5A7z1y8SDHw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "async-retry": "^1.3.3", - "is-buffer": "^2.0.5", - "is-node-process": "^1.2.0", - "throttleit": "^2.1.0", - "undici": "^6.23.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6033,51 +5858,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -6401,21 +6181,6 @@ "node": ">=12.0.0" } }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -6426,17 +6191,6 @@ "node": ">= 0.4" } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "retry": "0.13.1" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6444,18 +6198,6 @@ "dev": true, "license": "MIT" }, - "node_modules/atomically": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", - "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "stubborn-fs": "^2.0.0", - "when-exit": "^2.1.4" - } - }, "node_modules/autoprefixer": { "version": "10.4.27", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", @@ -7157,14 +6899,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chance": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.13.tgz", - "integrity": "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -7175,17 +6909,6 @@ "node": ">=10" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -7235,19 +6958,6 @@ "dev": true, "license": "MIT" }, - "node_modules/class-validator": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", - "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/validator": "^13.15.3", - "libphonenumber-js": "^1.11.1", - "validator": "^13.15.20" - } - }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -7531,71 +7241,6 @@ "dev": true, "license": "MIT" }, - "node_modules/conf": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz", - "integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "atomically": "^2.0.3", - "debounce-fn": "^6.0.0", - "dot-prop": "^10.0.0", - "env-paths": "^3.0.0", - "json-schema-typed": "^8.0.1", - "semver": "^7.7.2", - "uint8array-extras": "^1.5.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/conf/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/conf/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/conf/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -7783,17 +7428,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -8359,23 +7993,6 @@ "dev": true, "license": "MIT" }, - "node_modules/debounce-fn": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", - "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -8632,14 +8249,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -8743,40 +8352,6 @@ "tslib": "^2.0.3" } }, - "node_modules/dot-prop": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", - "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "type-fest": "^5.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dot-prop/node_modules/type-fest": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", - "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "peer": true, - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -8879,21 +8454,7 @@ "node": ">=0.12" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/envinfo": { @@ -11218,18 +10779,6 @@ "node": ">=10.13.0" } }, - "node_modules/inversify": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.1.4.tgz", - "integrity": "sha512-PbxrZH/gTa1fpPEEGAjJQzK8tKMIp5gRg6EFNJlCtzUcycuNdmhv3uk5P8Itm/RIjgHJO16oQRLo9IHzQN51bA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@inversifyjs/common": "1.3.3", - "@inversifyjs/core": "1.3.4" - } - }, "node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", @@ -11240,24 +10789,6 @@ "node": ">= 10" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -11349,31 +10880,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -11571,24 +11077,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -11615,14 +11103,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -11979,103 +11459,6 @@ "node": ">= 0.4" } }, - "node_modules/javascript-obfuscator": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-5.3.0.tgz", - "integrity": "sha512-EKKLTeKgzmY/foePpuNWhlKnrSqWMt1IHvIp0FEWzXODFqY2ZB6Mr0s2Q0uIOFCYbdYHeJNdPs6wpPb9NoUxMA==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@javascript-obfuscator/escodegen": "2.3.1", - "@javascript-obfuscator/estraverse": "5.4.0", - "@vercel/blob": ">=0.23.0", - "acorn": "8.15.0", - "assert": "2.1.0", - "chalk": "4.1.2", - "chance": "1.1.13", - "class-validator": "0.14.3", - "commander": "12.1.0", - "conf": "15.0.2", - "eslint-scope": "8.4.0", - "eslint-visitor-keys": "4.2.1", - "fast-deep-equal": "3.1.3", - "inversify": "6.1.4", - "js-string-escape": "1.0.1", - "md5": "2.3.0", - "mkdirp": "3.0.1", - "multimatch": "5.0.0", - "process": "0.11.10", - "reflect-metadata": "0.2.2", - "source-map-support": "0.5.21", - "string-template": "1.0.0", - "stringz": "2.1.0", - "tslib": "2.8.1" - }, - "bin": { - "javascript-obfuscator": "bin/javascript-obfuscator" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/javascript-obfuscator/node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/javascript-obfuscator/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/javascript-obfuscator/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/javascript-obfuscator/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -13187,17 +12570,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13298,14 +12670,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -13420,14 +12784,6 @@ "node": ">= 0.8.0" } }, - "node_modules/libphonenumber-js": { - "version": "1.12.40", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.40.tgz", - "integrity": "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -13719,17 +13075,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -13779,27 +13124,6 @@ "node": ">= 0.4" } }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/md5/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -13994,23 +13318,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -14318,24 +13625,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -14918,9 +14207,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -14938,7 +14227,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -16508,55 +15797,6 @@ "renderkid": "^3.0.0" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -18383,14 +17623,6 @@ "node": ">=10" } }, - "node_modules/string-template": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-1.0.0.tgz", - "integrity": "sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/string-width": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", @@ -18535,17 +17767,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/stringz": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/stringz/-/stringz-2.1.0.tgz", - "integrity": "sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "char-regex": "^1.0.2" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -18605,25 +17826,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stubborn-fs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", - "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "stubborn-utils": "^1.0.1" - } - }, - "node_modules/stubborn-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", - "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -18790,20 +17992,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tailwindcss": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.7.tgz", @@ -19115,20 +18303,6 @@ "tslib": "^2" } }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -19499,20 +18673,6 @@ "node": ">= 0.8" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -19532,17 +18692,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -19674,21 +18823,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19742,17 +18876,6 @@ "node": ">=10.12.0" } }, - "node_modules/validator": { - "version": "13.15.26", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", - "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -20332,14 +19455,6 @@ "node": ">=12" } }, - "node_modules/when-exit": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", - "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 607f58ea..2344ee50 100644 --- a/ui/package.json +++ b/ui/package.json @@ -65,7 +65,7 @@ "jest-environment-jsdom": "^29.7.0", "lint-staged": "^16.4.0", "mini-css-extract-plugin": "^2.9.0", - "postcss": "8.4", + "postcss": "^8.5.10", "postcss-loader": "7.3", "postcss-preset-env": "8.4", "prettier": "^2.7.1", @@ -140,6 +140,7 @@ "follow-redirects": "1.16.0", "dompurify": "^3.4.0", "protobufjs": "^7.5.5", - "uuid": "14.0.0" + "uuid": "14.0.0", + "postcss": "^8.5.10" } } \ No newline at end of file diff --git a/ui/src/components/Connector/ManualGiteaConnector.tsx b/ui/src/components/Connector/ManualGiteaConnector.tsx index a192fa99..271d38be 100644 --- a/ui/src/components/Connector/ManualGiteaConnector.tsx +++ b/ui/src/components/Connector/ManualGiteaConnector.tsx @@ -125,6 +125,16 @@ const ManualGiteaConnector: React.FC = () => {
  • Scopes: repository read (and PR read if available)
  • Use a dedicated service user (recommended)
  • +
    + + Open full guide ↗ + +