Files
mygit/apps/comments-ui/vite-plugin-strip-fingerprinting.ts
DuckQ1u 93d1b7c3d3
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
first commit
2026-04-22 19:51:20 +07:00

192 lines
7.7 KiB
TypeScript

import type {Plugin} from 'vite';
interface Replacement {
search: string;
replace: string;
description: string;
}
interface PatternGroup {
filePattern: RegExp;
replacements: Replacement[];
}
/**
* Vite plugin that patches ProseMirror and tiptap browser detection to avoid
* accessing high-entropy fingerprinting APIs (navigator.vendor,
* navigator.platform, navigator.maxTouchPoints).
*
* DuckDuckGo's Tracker Radar classifies scripts that access these APIs as
* fingerprinting (score 3 = maximum). Safari's Advanced Fingerprinting
* Protection (on by default since Safari 26, Sept 2025) uses this data to
* restrict API access and storage for scripts from flagged domains like
* cdn.jsdelivr.net.
*
* This plugin replaces those API accesses with equivalent checks using only
* navigator.userAgent, which has a much lower fingerprinting weight.
*
* Affected packages:
* - prosemirror-view: navigator.vendor, navigator.platform, navigator.maxTouchPoints
* - prosemirror-keymap: navigator.platform
* - prosemirror-commands: navigator.platform
* - @tiptap/core: navigator.platform
* - w3c-keyname: navigator.platform
*/
export function stripFingerprintingPlugin(): Plugin {
const patternGroups: PatternGroup[] = [
{
filePattern: /prosemirror-view[\\/]dist[\\/]index\.js$/,
replacements: [
{
// Safari detection: nav.vendor → userAgent check
// Original: checks if vendor is "Apple Computer"
// Patched: checks UA for Safari without Chrome/Chromium
search: '/Apple Computer/.test(nav.vendor)',
replace: '/Safari\\//.test(agent) && !/Chrome\\//.test(agent) && !/Chromium\\//.test(agent)',
description: 'prosemirror-view: safari detection (nav.vendor)'
},
{
// iOS detection: remove nav.maxTouchPoints fallback
// Original: detects iPadOS via maxTouchPoints > 2
// Patched: relies on Mobile/xxx in UA only
// Trade-off: iPadOS 13+ sends desktop Mac UA, so it won't
// be detected as iOS. This is acceptable — iPad works fine
// with desktop Mac editor handling.
search: ' || !!nav && nav.maxTouchPoints > 2',
replace: '',
description: 'prosemirror-view: iOS detection (nav.maxTouchPoints)'
},
{
// Mac detection: nav.platform → userAgent check
search: 'nav ? /Mac/.test(nav.platform) : false',
replace: '/Macintosh/.test(agent)',
description: 'prosemirror-view: mac detection (nav.platform)'
},
{
// Windows detection: nav.platform → userAgent check
search: 'nav ? /Win/.test(nav.platform) : false',
replace: '/Windows/.test(agent)',
description: 'prosemirror-view: windows detection (nav.platform)'
}
]
},
{
filePattern: /prosemirror-keymap[\\/]dist[\\/]index\.js$/,
replacements: [
{
search: '/Mac|iP(hone|[oa]d)/.test(navigator.platform)',
replace: '/Macintosh|iPhone|iPad|iPod/.test(navigator.userAgent)',
description: 'prosemirror-keymap: mac/iOS detection (navigator.platform)'
}
]
},
{
filePattern: /prosemirror-commands[\\/]dist[\\/]index\.js$/,
replacements: [
{
search: '/Mac|iP(hone|[oa]d)/.test(navigator.platform)',
replace: '/Macintosh|iPhone|iPad|iPod/.test(navigator.userAgent)',
description: 'prosemirror-commands: mac/iOS detection (navigator.platform)'
}
]
},
{
filePattern: /w3c-keyname[\\/]index\.js$/,
replacements: [
{
search: '/Mac/.test(navigator.platform)',
replace: '/Macintosh/.test(navigator.userAgent)',
description: 'w3c-keyname: mac detection (navigator.platform)'
}
]
},
{
filePattern: /@tiptap[\\/]core[\\/]dist[\\/]index\.js$/,
replacements: [
{
// isAndroid: remove navigator.platform === 'Android' check,
// keep the userAgent fallback which already handles this
search: 'navigator.platform === \'Android\' || ',
replace: '',
description: '@tiptap/core: isAndroid (navigator.platform)'
},
{
// isiOS: replace navigator.platform array check with UA
// The array ['iPad Simulator', 'iPhone Simulator', ...] is
// still present but .includes() on it becomes a no-op.
// The UA check catches real iPhone/iPod devices.
// iPadOS 13+ is handled by the next line in tiptap:
// navigator.userAgent.includes('Mac') && 'ontouchend' in document
search: '].includes(navigator.platform)',
replace: '].length === 0 || /iPhone|iPod/.test(navigator.userAgent)',
description: '@tiptap/core: isiOS (navigator.platform)'
},
{
// isMacOS: replace navigator.platform with userAgent
search: '/Mac/.test(navigator.platform)',
replace: '/Macintosh/.test(navigator.userAgent)',
description: '@tiptap/core: isMacOS (navigator.platform)'
}
]
}
];
const appliedReplacements = new Map<string, Set<string>>();
return {
name: 'strip-fingerprinting',
enforce: 'pre',
buildStart() {
appliedReplacements.clear();
},
transform(code: string, id: string) {
const normalizedId = id.replace(/\\/g, '/');
const group = patternGroups.find(g => g.filePattern.test(normalizedId));
if (!group) {
return null;
}
let transformed = code;
let hasChanges = false;
for (const replacement of group.replacements) {
if (transformed.includes(replacement.search)) {
transformed = transformed.replaceAll(replacement.search, replacement.replace);
hasChanges = true;
if (!appliedReplacements.has(normalizedId)) {
appliedReplacements.set(normalizedId, new Set());
}
appliedReplacements.get(normalizedId)!.add(replacement.description);
}
}
if (hasChanges) {
return {code: transformed, map: null};
}
return null;
},
buildEnd() {
const allDescriptions = patternGroups.flatMap(g => g.replacements.map(r => r.description));
const applied = new Set(
[...appliedReplacements.values()].flatMap(s => [...s])
);
const missing = allDescriptions.filter(d => !applied.has(d));
if (missing.length > 0) {
this.warn(
`strip-fingerprinting: ${missing.length} replacement(s) did not match. ` +
`Dependencies may have been updated. Unmatched:\n` +
missing.map(d => ` - ${d}`).join('\n')
);
}
}
};
}