diff --git a/src/frontend/nginx-security-headers.conf b/src/frontend/nginx-security-headers.conf index 78a08a4..648ff29 100644 --- a/src/frontend/nginx-security-headers.conf +++ b/src/frontend/nginx-security-headers.conf @@ -5,4 +5,4 @@ add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always; # Script 'unsafe-inline' is needed by the inline theme script and Angular's inline loaders. -add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com https://*.google-analytics.com https://*.analytics.google.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; diff --git a/src/frontend/src/app/app.ts b/src/frontend/src/app/app.ts index 5a61a47..a25ddb3 100644 --- a/src/frontend/src/app/app.ts +++ b/src/frontend/src/app/app.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, afterNextRender, effect, inject, un import { RouterOutlet } from '@angular/router'; import { ToastComponent } from './shared/components/toast/toast'; import { ConfirmModalComponent } from './shared/components/confirm-modal/confirm-modal.component'; -import { AnalyticsService } from './core/services/analytics.service'; import { RouteLoaderComponent } from './shared/components/route-loader/route-loader'; import { PackageService } from './core/services/package.service'; import { SignalrService } from './core/services/signalr.service'; @@ -22,8 +21,6 @@ export class App { private liveUpdates = inject(LiveUpdatesService); constructor() { - inject(AnalyticsService).init(); - afterNextRender(() => { this.loadLiveUpdates(); this.signalr.startConnection().catch(() => undefined); diff --git a/src/frontend/src/app/core/services/analytics.service.ts b/src/frontend/src/app/core/services/analytics.service.ts deleted file mode 100644 index 8d59fd7..0000000 --- a/src/frontend/src/app/core/services/analytics.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, PLATFORM_ID, inject } from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; -import { Router, NavigationEnd } from '@angular/router'; -import { filter } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; - -type GtagArguments = [command: string, ...parameters: unknown[]]; - -interface AnalyticsWindow { - dataLayer?: unknown[]; - gtag?: (...args: GtagArguments) => void; -} - -@Injectable({ providedIn: 'root' }) -export class AnalyticsService { - private router = inject(Router); - private isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); - - // Lives as long as the application, so the subscription is never torn down - init(): void { - if (!this.isBrowser || !environment.gaId) return; - - const gtag = this.loadScript(environment.gaId); - - this.router.events.pipe( - filter((event): event is NavigationEnd => event instanceof NavigationEnd) - ).subscribe(event => { - if (event.urlAfterRedirects.startsWith('/admin')) return; - - gtag('event', 'page_view', { - page_path: event.urlAfterRedirects, - page_location: window.location.href, - page_title: document.title - }); - }); - } - - private loadScript(gaId: string): (...args: GtagArguments) => void { - const script = document.createElement('script'); - script.async = true; - script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`; - document.head.appendChild(script); - - const analytics = window as unknown as AnalyticsWindow; - const dataLayer = analytics.dataLayer ?? (analytics.dataLayer = []); - // gtag.js reads the queue entries as the arguments objects the snippet pushes, not as arrays - const gtag = function (..._args: GtagArguments) { - dataLayer.push(arguments); - }; - analytics.gtag = gtag; - - gtag('js', new Date()); - gtag('config', gaId, { send_page_view: false }); - return gtag; - } -} diff --git a/src/frontend/src/app/features/cookies/cookies.html b/src/frontend/src/app/features/cookies/cookies.html index 8a1f375..7100cec 100644 --- a/src/frontend/src/app/features/cookies/cookies.html +++ b/src/frontend/src/app/features/cookies/cookies.html @@ -11,17 +11,17 @@

1. Overview

- {{ appName }} uses browser local storage to enhance your user experience. We do not use - third-party cookies or tracking technologies. This policy explains what we store locally - in your browser and why. + {{ appName }} sets no cookies and uses no tracking technologies. It keeps a few preferences in + your browser's local storage so the site remembers them. This policy lists exactly what is + stored and why.

2. What We Store

- All data is stored locally in your browser using localStorage. Nothing is sent to - external servers except when explicitly requested by your actions (e.g., package searches). + These entries live only in your browser (localStorage, or sessionStorage where noted). They are + never sent to our servers or to anyone else.

@@ -47,6 +47,12 @@

2. What We Essential Token lifetime + + lastSelectedRegistry + The registry you last selected on the home page + Functional + Persistent + recentRegistries Recent registry selections for quick access @@ -55,25 +61,37 @@

2. What We pkgdiff-cookie-consent - Your cookie consent choice + Remembers that you dismissed the privacy notice Essential Persistent + + pkgdiff-linkedin-oauth-state + Protects the LinkedIn sign-in flow (admin only, sessionStorage) + Essential + Until the flow completes +

- All items marked as "Functional" or "Essential" are necessary for the Service to operate - correctly. We do not use analytics, advertising, or tracking cookies. + "Essential" entries are needed for the Service to work; "Functional" entries only remember your + choices. None of them identify or track you, so they need no consent.

-

3. Third-Party Cookies

+

3. Analytics and Third Parties

+

+ We count visits with Cloudflare Web Analytics, which sets no cookies, stores nothing in your + browser and does not track you across sites or visits. +

- {{ appName }} does not integrate with any third-party services that set cookies or track - users. All functionality is self-contained within our application. + No third-party service sets cookies on {{ appName }}. Your browser does contact Google Fonts and + GitHub to load fonts and images; see the + Privacy Policy + for what they receive.

@@ -88,8 +106,8 @@

4. Managin
  • Safari: Preferences → Privacy → Manage Website Data → pkgdiff.dev → Remove
  • - Clearing local storage will reset your theme preference and consent choice. Essential - functional data (like auth tokens) will also be removed, logging you out if applicable. + Clearing it resets your theme and registry choices and shows the privacy notice again. + Administrators are also signed out.

    @@ -97,8 +115,7 @@

    4. Managin

    5. Changes to This Policy

    We may update this Cookie Policy from time to time. Changes will be posted on this page - with an updated "Last updated" date. Continued use of {{ appName }} after changes - constitutes acceptance of the updated policy. + with an updated "Last updated" date.

    diff --git a/src/frontend/src/app/features/cookies/cookies.ts b/src/frontend/src/app/features/cookies/cookies.ts index 0d60ce9..99d4fcf 100644 --- a/src/frontend/src/app/features/cookies/cookies.ts +++ b/src/frontend/src/app/features/cookies/cookies.ts @@ -12,7 +12,7 @@ export class CookiesComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 9, 2026'; + lastUpdated = 'October 19, 2026'; ngOnInit() { this.seo.updateMeta({ diff --git a/src/frontend/src/app/features/privacy/privacy.html b/src/frontend/src/app/features/privacy/privacy.html index 92170b0..2852742 100644 --- a/src/frontend/src/app/features/privacy/privacy.html +++ b/src/frontend/src/app/features/privacy/privacy.html @@ -11,111 +11,216 @@

    1. Overview

    - {{ appName }} is committed to protecting your privacy. This policy explains what information we - collect, how we use it, and your rights regarding that information. We designed the Service to - minimize data collection — we only collect what is necessary to operate and improve {{ appName }}. + This policy explains what personal data {{ appName }} ("the Service", "we") processes when you + visit {{ siteUrl }}, why, for how long, and which rights you have. {{ appName }} needs no account + to use, sets no tracking cookies and shows no ads. We keep only what is needed to run the Service + and protect it from abuse.

    -

    2. Information We Collect

    +

    2. Who Is Responsible

    +

    + The data controller is {{ controllerName }}, the individual developer who operates {{ appName }} + (based in {{ controllerCountry }}). You can reach the controller about anything in this policy at + {{ contactEmail }}. +

    +
    + +
    +

    3. Information We Process

    + +

    3.1 Request Logs

    +

    + For every request to our API we record your IP address, your browser's user agent string, the + requested path, the HTTP method, the response status and the time. We use these logs to detect + abuse, enforce rate limits and diagnose errors. +

    + +

    3.2 Search Data

    +

    + When you compare package versions, we store the package name, the selected versions, + whether prereleases were included, the time, how long the analysis took and your IP address. + Package names and versions feed the public "recent" and "popular" lists; your IP address is never + shown publicly and is used only to spot automated abuse. +

    + +

    3.3 Rate Limiting and Blocking

    +

    + Short-lived counters keyed by IP address enforce rate limits and expire on their own. An IP address + that attacks or overloads the Service can be blocked, temporarily or permanently, together with the + reason for the block. +

    -

    2.1 Search Data

    +

    3.4 Analytics

    - When you search for a package, we store the package name, selected versions, and a timestamp - of the search. This data is used to display recent searches and improve the Service. + We measure traffic with Cloudflare Web Analytics. It sets no cookies, uses no browser storage and + does not fingerprint or follow individual visitors. It reports aggregated data such as the page + visited, the referring site, browser and device type, country and page load performance.

    -

    2.2 Technical Data

    +

    3.5 Administrator Accounts

    - We may collect your IP address, browser type, and general usage patterns through server logs. - This information is used for security monitoring, rate limiting, and diagnosing technical issues. + There is no public sign-up. The only accounts are those of the Service's administrators, for whom + we store a username, an email address and a hashed password, never the password itself.

    -

    2.3 Account Data

    +

    3.6 Email

    - If you create an account, we store your email address and an encrypted password. We never store - passwords in plain text. + If you write to us, we use your email address and message only to answer you.

    -

    3. What We Don't Collect

    +

    4. Why We Process It (Legal Basis)

    +

    + We do not sell your data, use it for advertising, build profiles of you, or make automated + decisions about you that have legal or similarly significant effects. +

    -

    4. How We Use Your Data

    - +

    5. How Long We Keep It

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DataRetention
    Request logsDeleted automatically after 30 days
    Search dataDeleted automatically after 90 days
    Rate-limit countersExpire on their own within minutes
    Temporary IP blocksRemoved once the block expires
    Permanent IP blocksKept until an administrator lifts the block or you ask us to review it
    Database backupsOverwritten after 14 days
    Server and container logsRotated automatically and overwritten after a short period
    Administrator accountsUntil the account is removed
    Email correspondenceAs long as needed to handle your request
    +
    -

    5. Data Storage & Retention

    -

    - Data is stored on secured servers. Cached package data is automatically purged after 30 days - and refreshed from upstream sources. Search history is retained for operational purposes and may - be periodically cleaned. You can request deletion of your data by contacting us. +

    6. Service Providers and International Transfers

    +

    We share personal data only with the providers needed to run the Service:

    + +

    + Cloudflare, Google and GitHub may process data outside your country, including in the United + States. Cloudflare processes it under its Data Processing Addendum, which includes the European + Commission's Standard Contractual Clauses.

    -
    - -
    -

    6. Third-Party Services

    -

    - {{ appName }} retrieves data from NuGet.org, npmjs.com, and GitHub to display package information. - When you search for a package, we make requests to these services on your behalf. These services - have their own privacy policies that govern how they handle data. +

    + To build release notes, our server fetches public data from NuGet.org, npmjs.com, PyPI, pub.dev and + GitHub, and security data from sources such as OSV and deps.dev. These requests come from our server + and contain package names and versions only — never your IP address or any other data about you.

    7. Your Rights

    +

    + Under the GDPR, the KVKK (Article 11) and similar laws, you can: +

    - To exercise any of these rights, contact us at + To use any of these rights, email {{ contactEmail }}. + Most of our records are keyed only by IP address, so please include the IP address(es) and the + approximate dates concerned. We answer within 30 days.

    -

    8. Changes to This Policy

    +

    8. Security

    - We may update this Privacy Policy from time to time. Changes will be posted on this page with - an updated revision date. Continued use of the Service after changes constitutes acceptance - of the revised policy. + Traffic is encrypted with TLS, the database is reachable only from inside the server, administrative + areas sit behind additional access controls, and backups are readable only by the server's owner.

    -

    9. Related Policies

    -

    - For complete transparency, please also review: +

    9. Children

    +

    + {{ appName }} is a developer tool and is not directed at children under 16. +

    +
    + +
    +

    10. Changes to This Policy

    +

    + We may update this policy when the Service changes. The new version is posted on this page with + an updated date.

    +
    + +
    +

    11. Related Policies

    -

    10. Contact

    +

    12. Contact

    - If you have questions about this Privacy Policy, please contact us at + Questions about this policy or your data: {{ contactEmail }}.

    diff --git a/src/frontend/src/app/features/privacy/privacy.ts b/src/frontend/src/app/features/privacy/privacy.ts index 2f95968..7657f7e 100644 --- a/src/frontend/src/app/features/privacy/privacy.ts +++ b/src/frontend/src/app/features/privacy/privacy.ts @@ -13,7 +13,10 @@ export class PrivacyComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 8, 2026'; + siteUrl = 'pkgdiff.dev'; + controllerName = 'Enes Yağız'; + controllerCountry = 'Türkiye'; + lastUpdated = 'October 19, 2026'; ngOnInit(): void { this.seo.updateMeta({ diff --git a/src/frontend/src/app/features/terms/terms.html b/src/frontend/src/app/features/terms/terms.html index c2f71dd..cbffc95 100644 --- a/src/frontend/src/app/features/terms/terms.html +++ b/src/frontend/src/app/features/terms/terms.html @@ -21,7 +21,7 @@

    1. Accepta

    2. Description of Service

    {{ appName }} is a free, open-source tool that allows users to compare package versions across - registries (such as NuGet and npm), view release notes, and analyze differences between versions. + registries (NuGet, npm, PyPI and pub.dev), view release notes, and analyze differences between versions. The Service retrieves publicly available data from package registries and GitHub repositories.

    @@ -42,16 +42,17 @@

    3. Accepta

    4. Data & Content

    The release notes, version information, and package metadata displayed on {{ appName }} are sourced - from third-party registries (NuGet.org, npmjs.com) and GitHub. We do not claim ownership of this - content. The accuracy, completeness, and timeliness of this data depend on the upstream sources. - {{ appName }} is not responsible for errors or omissions in third-party data. + from third-party registries (NuGet.org, npmjs.com, PyPI and pub.dev) and GitHub. We do not claim + ownership of this content; it belongs to its authors and is shown with a link to its source. The accuracy, completeness, and timeliness of this data depend on the upstream sources. + {{ appName }} is not responsible for errors or omissions in third-party data. If you own content + shown here and want it corrected or removed, contact us and we will act on it promptly.

    5. Intellectual Property

    - The {{ appName }} source code is available under its respective open-source license. The Service's + The {{ appName }} source code is available under the MIT License. The Service's branding, design, and original content are the property of the {{ appName }} project. Third-party package names, logos, and trademarks belong to their respective owners.

    diff --git a/src/frontend/src/app/features/terms/terms.ts b/src/frontend/src/app/features/terms/terms.ts index c07e431..17e24ab 100644 --- a/src/frontend/src/app/features/terms/terms.ts +++ b/src/frontend/src/app/features/terms/terms.ts @@ -13,7 +13,7 @@ export class TermsComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 8, 2026'; + lastUpdated = 'October 19, 2026'; ngOnInit(): void { this.seo.updateMeta({ diff --git a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html index 19f406c..6b6031d 100644 --- a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html +++ b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html @@ -6,30 +6,24 @@

    - We use local storage to enhance your experience. No third-party cookies or tracking. - - Learn more + Privacy Policy

    -
    diff --git a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts index fe95680..a47dd42 100644 --- a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts +++ b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts @@ -19,7 +19,7 @@ export class CookieConsentComponent { constructor() { const destroyRef = inject(DestroyRef); - // Only a browser knows whether the visitor has already chosen + // Only a browser knows whether the visitor has already dismissed the notice afterNextRender(() => { if (this.storage.get(CONSENT_KEY)) return; @@ -28,13 +28,8 @@ export class CookieConsentComponent { }); } - accept() { - this.storage.set(CONSENT_KEY, 'accepted'); - this.visible.set(false); - } - - decline() { - this.storage.set(CONSENT_KEY, 'declined'); + dismiss() { + this.storage.set(CONSENT_KEY, 'seen'); this.visible.set(false); } } diff --git a/src/frontend/src/environments/environment.prod.ts b/src/frontend/src/environments/environment.prod.ts index ec78052..45aabfc 100644 --- a/src/frontend/src/environments/environment.prod.ts +++ b/src/frontend/src/environments/environment.prod.ts @@ -1,6 +1,5 @@ export const environment = { production: true, apiUrl: '/api', - hubUrl: '/analysisHub', - gaId: 'G-TBR2L3QF2R' + hubUrl: '/analysisHub' }; diff --git a/src/frontend/src/environments/environment.ts b/src/frontend/src/environments/environment.ts index 35e9cd3..c7959d6 100644 --- a/src/frontend/src/environments/environment.ts +++ b/src/frontend/src/environments/environment.ts @@ -1,6 +1,5 @@ export const environment = { production: false, apiUrl: 'http://localhost:5054/api', - hubUrl: 'http://localhost:5054/analysisHub', - gaId: '' // GA4 tracking disabled in development + hubUrl: 'http://localhost:5054/analysisHub' };