From b6eba9d35dddd1cfe4647ac80c1f857e86490aca Mon Sep 17 00:00:00 2001 From: architect Date: Wed, 30 Sep 2026 15:22:32 -0400 Subject: [PATCH 1/3] refactor: remove dead EditPageLink.tsx duplicating DocsSourceActions logic EditPageLink.tsx was only imported by its own test file; DocsLayout.tsx renders DocsSourceActions.tsx instead, which supersedes it. The dead file kept an independent copy of security-sensitive path-sanitization and GitHub-edit-URL validation logic, risking silent drift from the live implementation. Signed-off-by: architect --- src/__tests__/EditPageLink.test.tsx | 142 --------------------------- src/components/docs/EditPageLink.tsx | 121 ----------------------- 2 files changed, 263 deletions(-) delete mode 100644 src/__tests__/EditPageLink.test.tsx delete mode 100644 src/components/docs/EditPageLink.tsx diff --git a/src/__tests__/EditPageLink.test.tsx b/src/__tests__/EditPageLink.test.tsx deleted file mode 100644 index 9ad5fe8..0000000 --- a/src/__tests__/EditPageLink.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -// @vitest-environment jsdom -// -// Covers src/components/docs/EditPageLink.tsx (previously 0%): -// - buildGitHubEditUrl: static fallback, shared-config override, path -// sanitization, unknown-project null -// - EditPageLink rendering: full and icon variants, and the -// isValidGitHubEditUrl XSS guard (protocol / hostname / path checks) -import React from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { cleanup, render, screen } from "@testing-library/react"; - -import type { SharedConfig } from "@/hooks/useSharedConfig"; - -const useSharedConfigMock = vi.fn<() => { config: SharedConfig | null }>( - () => ({ config: null }) -); - -vi.mock("@/hooks/useSharedConfig", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - useSharedConfig: () => useSharedConfigMock(), - }; -}); - -import EditPageLink, { - buildGitHubEditUrl, -} from "@/components/docs/EditPageLink"; -import type { ProjectId } from "@/config/versions"; - -function configWithEditBase(editBaseUrls: Record) { - return { config: { editBaseUrls } as unknown as SharedConfig }; -} - -beforeEach(() => { - cleanup(); - useSharedConfigMock.mockReset(); - useSharedConfigMock.mockReturnValue({ config: null }); -}); - -describe("buildGitHubEditUrl", () => { - it("falls back to the static edit base URL per project", () => { - expect(buildGitHubEditUrl("guide/intro.md", "hive")).toBe( - "https://github.com/hivecommons/hive/edit/v5/src/docs/guide/intro.md" - ); - expect(buildGitHubEditUrl("README.md", "pluk")).toBe( - "https://github.com/hivecommons/pluk/edit/main/README.md" - ); - }); - - it("prefers the shared-config override over the static base", () => { - const url = buildGitHubEditUrl("docs/a.md", "pluk", { - pluk: "https://github.com/hivecommons/pluk/edit/release-branch/docs", - }); - expect(url).toBe( - "https://github.com/hivecommons/pluk/edit/release-branch/docs/docs/a.md" - ); - }); - - it("still uses the static base when the override map lacks the project", () => { - const url = buildGitHubEditUrl("a.md", "dibs", { - pluk: "https://github.com/hivecommons/pluk/edit/release-branch/docs", - }); - expect(url).toBe("https://github.com/hivecommons/dibs/edit/main/a.md"); - }); - - it("returns null when no base URL exists for the project", () => { - expect(buildGitHubEditUrl("a.md", "nope" as ProjectId)).toBeNull(); - }); - - it("strips .. sequences and leading slashes from the file path", () => { - expect(buildGitHubEditUrl("../../etc/passwd", "hotshot")).toBe( - "https://github.com/hivecommons/hotshot/edit/main/etc/passwd" - ); - expect(buildGitHubEditUrl("///docs/x.md", "hotshot")).toBe( - "https://github.com/hivecommons/hotshot/edit/main/docs/x.md" - ); - }); -}); - -describe("EditPageLink rendering", () => { - it("renders the full variant with a validated GitHub href", () => { - render(); - const link = screen.getByRole("link", { - name: /edit this page on github/i, - }); - expect(link).toHaveProperty( - "href", - "https://github.com/hivecommons/hive/edit/v5/src/docs/guide/intro.md" - ); - expect(link.getAttribute("target")).toBe("_blank"); - expect(link.getAttribute("rel")).toBe("noopener noreferrer"); - }); - - it("renders the icon variant with a title instead of text", () => { - render( - - ); - const link = screen.getByTitle("Edit this page on GitHub"); - expect(link).toHaveProperty( - "href", - "https://github.com/hivecommons/pluk/edit/main/a.md" - ); - expect(link.textContent).toBe(""); - }); - - it("uses the shared-config edit base when provided", () => { - useSharedConfigMock.mockReturnValue( - configWithEditBase({ - pluk: "https://github.com/hivecommons/pluk/edit/release-branch/docs", - }) - ); - render(); - const link = screen.getByRole("link", { - name: /edit this page on github/i, - }); - expect(link).toHaveProperty( - "href", - "https://github.com/hivecommons/pluk/edit/release-branch/docs/guide/intro.md" - ); - }); - - it.each([ - ["non-github host", "https://evil.example.com/edit/main"], - ["plain http", "http://github.com/hivecommons/hive/edit/v5"], - ["missing /edit/ path", "https://github.com/hivecommons/hive/blob/main"], - ["javascript scheme", "javascript:alert(1)//edit/"], - ])("renders nothing for an unsafe base URL (%s)", (_name, base) => { - useSharedConfigMock.mockReturnValue(configWithEditBase({ hive: base })); - const { container } = render( - - ); - expect(container.innerHTML).toBe(""); - }); - - it("renders nothing when no base URL resolves at all", () => { - const { container } = render( - - ); - expect(container.innerHTML).toBe(""); - }); -}); diff --git a/src/components/docs/EditPageLink.tsx b/src/components/docs/EditPageLink.tsx deleted file mode 100644 index 7f1f4ca..0000000 --- a/src/components/docs/EditPageLink.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; - -import { useSharedConfig } from '@/hooks/useSharedConfig'; -import type { ProjectId } from '@/config/versions'; - -// See DocsSourceActions.tsx for the full rationale; kept in sync with -// scripts/sync-hive-docs.ts. -const STATIC_EDIT_BASE_URLS: Record = { - hive: 'https://github.com/hivecommons/hive/edit/v5/src/docs', - hotshot: 'https://github.com/hivecommons/hotshot/edit/main', - pluk: 'https://github.com/hivecommons/pluk/edit/main', - rationguard: 'https://github.com/hivecommons/rationguard/edit/main', - promptargs: 'https://github.com/hivecommons/promptargs/edit/main', - dibs: 'https://github.com/hivecommons/dibs/edit/main', - spektacular: 'https://github.com/hivecommons/spektacular/edit/main', -}; - -interface EditPageLinkProps { - filePath: string; - projectId: ProjectId; - variant?: 'full' | 'icon'; -} - -// Validate that URL is a safe GitHub edit URL to prevent XSS -function isValidGitHubEditUrl(url: string): boolean { - try { - const parsed = new URL(url); - // Only allow https GitHub URLs with /edit/ path - return ( - parsed.protocol === 'https:' && - parsed.hostname === 'github.com' && - parsed.pathname.includes('/edit/') - ); - } catch { - return false; - } -} - -//refactoring helper function to build the GitHub edit URL -export function buildGitHubEditUrl( - filePath: string, - projectId: ProjectId, - editBaseUrls?: Record -): string | null { - const baseUrl = editBaseUrls?.[projectId] ?? STATIC_EDIT_BASE_URLS[projectId]; - - if (!baseUrl) return null; - - const sanitizedFilePath = filePath.replace(/\.\./g, "").replace(/^\/+/, ""); - return `${baseUrl}/${sanitizedFilePath}`; -} - -export function EditPageLink({ filePath, projectId, variant = 'full' }: EditPageLinkProps) { - const { config } = useSharedConfig(); - - const editUrl = buildGitHubEditUrl( - filePath, - projectId, - config?.editBaseUrls - ); - - if (!editUrl) return null; - - - // Validate URL before rendering to prevent XSS - if (!isValidGitHubEditUrl(editUrl)) return null; - - // Use validated URL object to construct safe href - // CodeQL: URL is validated above to only allow https://github.com with /edit/ path - const safeUrl = new URL(editUrl); - - // Pencil icon SVG - const PencilIcon = () => ( - - - - ); - - // Icon-only variant for top right placement - if (variant === 'icon') { - return ( - - - - ); - } - - // Full variant (original) for bottom of page - return ( - - ); -} - -export default EditPageLink; From 3ebbee01479c9acee11cf35facab973a567f93cc Mon Sep 17 00:00:00 2001 From: architect Date: Wed, 30 Sep 2026 21:06:49 -0400 Subject: [PATCH 2/3] changelog: add fragment for dead EditPageLink.tsx removal Signed-off-by: architect --- changelog.d/removed-dead-editpagelink.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/removed-dead-editpagelink.md diff --git a/changelog.d/removed-dead-editpagelink.md b/changelog.d/removed-dead-editpagelink.md new file mode 100644 index 0000000..88f6c65 --- /dev/null +++ b/changelog.d/removed-dead-editpagelink.md @@ -0,0 +1 @@ +- Remove dead `EditPageLink.tsx` and its test, which duplicated security-sensitive URL-validation and path-sanitization logic already live in `DocsSourceActions.tsx`. From af6892c47984ba9ef3b5fca376a978c14c563888 Mon Sep 17 00:00:00 2001 From: architect Date: Wed, 30 Sep 2026 21:06:54 -0400 Subject: [PATCH 3/3] changelog: rename fragment to use valid 'changed' category Signed-off-by: architect --- ...d-dead-editpagelink.md => changed-remove-dead-editpagelink.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{removed-dead-editpagelink.md => changed-remove-dead-editpagelink.md} (100%) diff --git a/changelog.d/removed-dead-editpagelink.md b/changelog.d/changed-remove-dead-editpagelink.md similarity index 100% rename from changelog.d/removed-dead-editpagelink.md rename to changelog.d/changed-remove-dead-editpagelink.md