Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions src/app/[lang]/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from "next/link"
import { useLang } from "@/core/i18n/lang-provider"
import { TOOL_REGISTRY } from "@/core/registry"
import { getExternalRequestToolDisclosures } from "@/core/registry/privacy"
import { LocalDataControls } from "@/features/privacy/local-data-controls"

export default function PrivacyPage() {
Expand All @@ -17,7 +18,14 @@ export default function PrivacyPage() {
{ title: p.privacy_contact_title, desc: p.privacy_contact_desc },
]
const toolTranslations = t.tools as Record<string, { title?: string }>
const externalRequestTools = TOOL_REGISTRY.filter((tool) => tool.privacy.externalRequest.required)
const externalRequestTools = getExternalRequestToolDisclosures(TOOL_REGISTRY)
.map((item) => ({
...item,
title: toolTranslations[item.tool.key]?.title ?? item.tool.slug,
purpose: t.common.external_network_notice.purposes?.[item.purposeKey as keyof typeof t.common.external_network_notice.purposes],
dataSentLabel: t.common.external_network_notice.external_data?.[item.dataSent as keyof typeof t.common.external_network_notice.external_data],
}))
.sort((a, b) => a.title.localeCompare(b.title, lang))

return (
<div className="mx-auto w-full max-w-5xl space-y-6">
Expand All @@ -43,19 +51,33 @@ export default function PrivacyPage() {
<h2 className="text-lg font-semibold">{p.privacy_external_request_tools_title}</h2>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{p.privacy_external_request_tools_desc}</p>
<div className="mt-4 grid gap-3">
{externalRequestTools.map((tool) => (
{externalRequestTools.map(({ tool, title, hosts, purpose, dataSentLabel, disclosure }) => (
<div key={tool.key} className="rounded-xl border border-border/70 bg-card/45 p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-medium">{toolTranslations[tool.key]?.title ?? tool.slug}</p>
<p className="text-sm font-medium">{title}</p>
<span className="rounded-md border border-amber-500/35 bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300">
{t.common.capability_external_request}
</span>
</div>
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
{tool.privacy.externalRequest.disclosure}
{disclosure}
</p>
<p className="mt-2 font-mono text-xs text-muted-foreground">
{(tool.privacy.externalRequest.domains ?? []).join(", ")}
<dl className="mt-2 grid gap-1 text-xs text-muted-foreground sm:grid-cols-3">
<div>
<dt className="font-medium text-foreground">{t.common.external_network_notice.hosts_label}</dt>
<dd className="mt-1 font-mono">{hosts.join(", ")}</dd>
</div>
<div>
<dt className="font-medium text-foreground">{t.common.external_network_notice.purpose_label}</dt>
<dd className="mt-1">{purpose}</dd>
</div>
<div>
<dt className="font-medium text-foreground">{t.common.external_network_notice.data_sent_label}</dt>
<dd className="mt-1">{dataSentLabel}</dd>
</div>
</dl>
<p className="mt-2 text-xs text-muted-foreground">
{t.common.external_network_notice.consent_required_message}
</p>
</div>
))}
Expand Down
26 changes: 11 additions & 15 deletions src/app/[lang]/trust-center/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CheckCircle2, ExternalLink, FileText, Github, LockKeyhole, Network, Shi
import { isValidLocale, requireTranslationValue } from "@/core/i18n/i18n"
import { getTranslation } from "@/core/i18n/translations/catalog"
import { TOOL_REGISTRY } from "@/core/registry"
import { getExternalRequestToolDisclosures } from "@/core/registry/privacy"
import { JsonLdScript } from "@/core/seo/components/json-ld-script"
import { SITE_URL, buildCanonicalUrl } from "@/core/seo/urls"

Expand Down Expand Up @@ -50,17 +51,12 @@ export default async function TrustCenterPage({
const p = t.pages
const common = t.common
const toolCopy = t.tools as Record<string, { title?: string }>
const externalTools = TOOL_REGISTRY
.filter((tool) => tool.privacy.externalRequest.required)
.map((tool) => ({
tool,
title: requireTranslationValue(toolCopy[tool.key]?.title, `tools.${tool.key}.title`),
purpose: tool.privacy.externalRequest.purposeKey
? common.external_network_notice.purposes?.[tool.privacy.externalRequest.purposeKey as keyof typeof common.external_network_notice.purposes]
: undefined,
dataSent: tool.privacy.externalRequest.userDataSent
? common.external_network_notice.external_data?.[tool.privacy.externalRequest.userDataSent as keyof typeof common.external_network_notice.external_data]
: undefined,
const externalTools = getExternalRequestToolDisclosures(TOOL_REGISTRY)
.map((item) => ({
...item,
title: requireTranslationValue(toolCopy[item.tool.key]?.title, `tools.${item.tool.key}.title`),
purpose: common.external_network_notice.purposes?.[item.purposeKey as keyof typeof common.external_network_notice.purposes],
dataSentLabel: common.external_network_notice.external_data?.[item.dataSent as keyof typeof common.external_network_notice.external_data],
}))
.sort((a, b) => a.title.localeCompare(b.title, locale))

Expand Down Expand Up @@ -279,18 +275,18 @@ export default async function TrustCenterPage({
</tr>
</thead>
<tbody className="divide-y divide-border/70 bg-card/35">
{externalTools.map(({ tool, title, purpose, dataSent }) => (
{externalTools.map(({ tool, title, hosts, purpose, dataSentLabel, disclosure }) => (
<tr key={tool.key}>
<td className="px-3 py-3 font-medium text-foreground">
<Link className="hover:text-primary" href={`/${locale}/${tool.slug}`}>
{title}
</Link>
</td>
<td className="px-3 py-3 font-mono text-xs text-muted-foreground">
{(tool.privacy.externalRequest.domains ?? []).join(", ")}
{hosts.join(", ")}
</td>
<td className="px-3 py-3 text-muted-foreground">{purpose ?? tool.privacy.externalRequest.disclosure}</td>
<td className="px-3 py-3 text-muted-foreground">{dataSent}</td>
<td className="px-3 py-3 text-muted-foreground">{purpose ?? disclosure}</td>
<td className="px-3 py-3 text-muted-foreground">{dataSentLabel}</td>
</tr>
))}
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion src/core/registry/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { CATEGORIES, type ToolCategory } from "./categories"
export { TOOL_MANIFESTS } from "./manifests"
export { getToolPrivacyNetworkMetadata } from "./privacy"
export { getExternalRequestToolDisclosures, getToolPrivacyNetworkMetadata } from "./privacy"
export { getRelatedTools } from "./related-tools"
export { TOOL_REGISTRY, TOOL_REGISTRY_ORDER, TOOLS_BY_KEY, getToolByKey, getToolBySlug, getToolsByCategory } from "./registry"
export { formatToolRegistryStatsTemplate, getToolRegistryStats, type ToolRegistryStats } from "./stats"
Expand Down
23 changes: 23 additions & 0 deletions src/core/registry/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,26 @@ export function getToolPrivacyNetworkMetadata(privacy: ToolPrivacyManifest): Too
externalDataSent: privacy.externalRequest.userDataSent as ToolExternalDataSent | undefined,
}
}

export type ExternalRequestToolDisclosure = {
tool: ToolMeta
hosts: readonly string[]
purposeKey: string
dataSent: ToolExternalDataSent
disclosure: string
}

export function getExternalRequestToolDisclosures(tools: readonly ToolMeta[]): ExternalRequestToolDisclosure[] {
return tools
.filter((tool) => tool.privacy.externalRequest.required)
.map((tool) => {
const externalRequest = tool.privacy.externalRequest
return {
tool,
hosts: externalRequest.domains ?? [],
purposeKey: externalRequest.purposeKey ?? "",
dataSent: externalRequest.userDataSent ?? "none",
disclosure: externalRequest.disclosure ?? "",
}
})
}
28 changes: 14 additions & 14 deletions src/lib/sitemap-lastmod.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,22 +220,22 @@
"fr": "2026-06-25T00:00:00.000Z"
},
"privacy": {
"en": "2026-06-20T00:00:00.000Z",
"zh-CN": "2026-06-20T00:00:00.000Z",
"zh-TW": "2026-06-20T00:00:00.000Z",
"ja": "2026-06-20T00:00:00.000Z",
"ko": "2026-06-20T00:00:00.000Z",
"de": "2026-06-20T00:00:00.000Z",
"fr": "2026-06-20T00:00:00.000Z"
"en": "2026-06-26T00:00:00.000Z",
"zh-CN": "2026-06-26T00:00:00.000Z",
"zh-TW": "2026-06-26T00:00:00.000Z",
"ja": "2026-06-26T00:00:00.000Z",
"ko": "2026-06-26T00:00:00.000Z",
"de": "2026-06-26T00:00:00.000Z",
"fr": "2026-06-26T00:00:00.000Z"
},
"trust-center": {
"en": "2026-06-24T00:00:00.000Z",
"zh-CN": "2026-06-24T00:00:00.000Z",
"zh-TW": "2026-06-24T00:00:00.000Z",
"ja": "2026-06-24T00:00:00.000Z",
"ko": "2026-06-24T00:00:00.000Z",
"de": "2026-06-24T00:00:00.000Z",
"fr": "2026-06-24T00:00:00.000Z"
"en": "2026-06-26T00:00:00.000Z",
"zh-CN": "2026-06-26T00:00:00.000Z",
"zh-TW": "2026-06-26T00:00:00.000Z",
"ja": "2026-06-26T00:00:00.000Z",
"ko": "2026-06-26T00:00:00.000Z",
"de": "2026-06-26T00:00:00.000Z",
"fr": "2026-06-26T00:00:00.000Z"
},
"roadmap": {
"en": "2026-06-25T00:00:00.000Z",
Expand Down
8 changes: 8 additions & 0 deletions tests/component/privacy-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,13 @@ describe("PrivacyPage", () => {
expect(screen.getByText("Instagram Photo Downloader")).toBeInTheDocument()
expect(screen.getByText("youtube.com, youtube-nocookie.com, youtu.be, i.ytimg.com")).toBeInTheDocument()
expect(screen.getByText("instagram.com")).toBeInTheDocument()
expect(screen.getAllByText("Hosts").length).toBeGreaterThan(0)
expect(screen.getAllByText("Purpose").length).toBeGreaterThan(0)
expect(screen.getAllByText("Data sent").length).toBeGreaterThan(0)
expect(screen.getAllByText("Generate and preview public thumbnail image URLs derived from the video link you enter.").length).toBeGreaterThan(0)
expect(screen.getByText("Download media from a URL you provide after you confirm you are allowed to use it.")).toBeInTheDocument()
expect(screen.getAllByText("A derived public asset URL may be requested by your browser.").length).toBeGreaterThan(0)
expect(screen.getByText("The URL you provide may be requested by your browser.")).toBeInTheDocument()
expect(screen.getAllByText("Network access starts only after you choose the external-request action.").length).toBeGreaterThan(0)
})
})
7 changes: 4 additions & 3 deletions tests/component/trust-center-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"
import TrustCenterPage from "@/app/[lang]/trust-center/page"
import { getTranslation } from "@/core/i18n/translations/catalog"
import { TOOL_REGISTRY } from "@/core/registry"
import { getExternalRequestToolDisclosures } from "@/core/registry/privacy"

vi.mock("next/link", () => ({
default: ({ href, children, ...props }: { href: string; children: React.ReactNode }) => (
Expand All @@ -29,14 +30,14 @@ describe("TrustCenterPage", () => {
expect(screen.getByRole("rowheader", { name: "Pipeline Builder" })).toBeInTheDocument()
expect(screen.getByRole("rowheader", { name: "External-request tools" })).toBeInTheDocument()

const externalTools = TOOL_REGISTRY.filter((tool) => tool.privacy.externalRequest.required)
const externalTools = getExternalRequestToolDisclosures(TOOL_REGISTRY)
const toolCopy = getTranslation("en").tools as Record<string, { title?: string }>
const table = screen.getByRole("table", { name: "External request tools" })

for (const tool of externalTools) {
for (const { tool, hosts } of externalTools) {
const title = toolCopy[tool.key]?.title ?? tool.slug
expect(within(table).getByRole("link", { name: title })).toHaveAttribute("href", `/en/${tool.slug}`)
for (const domain of tool.privacy.externalRequest.domains ?? []) {
for (const domain of hosts) {
expect(within(table).getByText(new RegExp(domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))).toBeInTheDocument()
}
}
Expand Down
8 changes: 8 additions & 0 deletions tests/guards/tool-network-access-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,12 @@ describe("tool network access metadata", () => {

expect(offenders).toEqual([])
})

it("keeps Privacy Policy and Trust Center external request lists on the shared disclosure source", () => {
const trustCenterPage = fs.readFileSync(path.join(ROOT, "src/app/[lang]/trust-center/page.tsx"), "utf8")
const privacyPage = fs.readFileSync(path.join(ROOT, "src/app/[lang]/privacy/page.tsx"), "utf8")

expect(trustCenterPage).toContain("getExternalRequestToolDisclosures(TOOL_REGISTRY)")
expect(privacyPage).toContain("getExternalRequestToolDisclosures(TOOL_REGISTRY)")
})
})