Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add temporary unprotected UA mitigations
  • Loading branch information
cursoragent committed Jul 3, 2026
commit 2d599321400a928e57ccaa57d56443cf40385336
6 changes: 6 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
addHashToFeatures,
stripReasons,
getBaseFeatureConfigs,
addUnprotectedTemporaryUserAgentMitigations,
readJsoncFile,
} from './util.js';

Expand Down Expand Up @@ -371,6 +372,11 @@ async function buildPlatforms() {
}
}

addUnprotectedTemporaryUserAgentMitigations(platform, platformConfig, [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of the same if statement above?

        if (platformOverride.unprotectedTemporary) {

...listData.exceptions,
...(platformOverride.unprotectedTemporary || []),
]);

if (platformOverride.experimentalVariants) {
platformConfig.experimentalVariants = platformOverride.experimentalVariants;
}
Expand Down
164 changes: 164 additions & 0 deletions tests/build-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mergeEventHubTelemetry,
mergeInterferenceTypes,
addHashToFeatures,
addUnprotectedTemporaryUserAgentMitigations,
} from '../util.js';

const ta1 = {
Expand Down Expand Up @@ -327,6 +328,169 @@ describe('mergeInterferenceTypes', () => {
});
});

describe('addUnprotectedTemporaryUserAgentMitigations', () => {
const exceptions = [
{
domain: 'global.example',
reason: 'Global temporary mitigation',
},
{
domain: 'platform.example',
reason: 'Platform temporary mitigation',
},
];

it('adds Safari-like custom user-agent entries on iOS', () => {
const config = {
features: {
customUserAgent: {
settings: {
ddgFixedSites: [
{
domain: 'global.example',
reason: 'Manual override',
},
],
omitApplicationSites: [],
},
},
},
};

addUnprotectedTemporaryUserAgentMitigations('ios', config, exceptions);

expect(config.features.customUserAgent.settings.ddgFixedSites).to.deep.equal([
{
domain: 'global.example',
reason: 'Manual override',
},
{
domain: 'platform.example',
reason: 'Platform temporary mitigation',
},
]);
expect(config.features.customUserAgent.settings.omitApplicationSites).to.deep.equal(exceptions);
});

it('adds Safari-like custom user-agent entries on macOS', () => {
const config = {
features: {
customUserAgent: {
settings: {
defaultSites: [],
},
},
},
};

addUnprotectedTemporaryUserAgentMitigations('macos', config, exceptions);

expect(config.features.customUserAgent.settings.defaultSites).to.deep.equal(exceptions);
});

it('adds Chrome client-brand hints on Android', () => {
const config = {
features: {
clientBrandHint: {
settings: {
domains: [
{
domain: 'global.example',
brand: 'DDG',
},
],
},
},
},
};

addUnprotectedTemporaryUserAgentMitigations('android', config, exceptions);

expect(config.features.clientBrandHint.settings.domains).to.deep.equal([
{
domain: 'global.example',
brand: 'DDG',
},
{
domain: 'platform.example',
brand: 'CHROME',
},
]);
});

it('adds Chrome UA and client-hint entries on Windows', () => {
const config = {
features: {
customUserAgent: {
features: {
userAgentStrategies: {
settings: {
strategies: [],
},
},
},
},
clientBrandHint: {
settings: {
domains: [],
},
},
uaChBrands: {
exceptions: [
{
domain: 'global.example',
},
{
domain: 'platform.example',
},
],
settings: {
conditionalChanges: [
{
condition: {
domain: 'global.example',
},
patchSettings: [
{
op: 'add',
path: '/brandName',
value: 'Google Chrome',
},
],
},
],
},
},
},
};

addUnprotectedTemporaryUserAgentMitigations('windows', config, exceptions);

expect(config.features.customUserAgent.features.userAgentStrategies.settings.strategies).to.deep.equal([
{
strategy: 'ChromeUA',
domain: 'global.example',
},
{
strategy: 'ChromeUA',
domain: 'platform.example',
},
]);
expect(config.features.clientBrandHint.settings.domains).to.deep.equal([
{
domain: 'global.example',
brand: 'Google Chrome',
},
{
domain: 'platform.example',
brand: 'Google Chrome',
},
]);
expect(config.features.uaChBrands.exceptions).to.deep.equal([]);
expect(config.features.uaChBrands.settings.conditionalChanges).to.have.length(2);
});
});

const mkRule = (rulePath, domains, reason) => {
return {
rule: rulePath,
Expand Down
52 changes: 52 additions & 0 deletions tests/output-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,56 @@ describe('Build output validation', () => {
});
});
});

describe('unprotected temporary user-agent mitigations', () => {
const extractDomains = (entry) => entry.domain;
const globalUnprotected = loadJSON('features/unprotected-temporary.json').exceptions.map(extractDomains);

it('adds iOS Safari-like custom user-agent settings', () => {
const config = loadJSON('generated/v5/ios-config.json');
const settings = config.features.customUserAgent.settings;
const ddgFixedSites = settings.ddgFixedSites.map(extractDomains);
const omitApplicationSites = settings.omitApplicationSites.map(extractDomains);

globalUnprotected.forEach((domain) => {
expect(ddgFixedSites).to.contain(domain);
expect(omitApplicationSites).to.contain(domain);
});
});

it('adds macOS Safari-like custom user-agent settings', () => {
const config = loadJSON('generated/v5/macos-config.json');
const defaultSites = config.features.customUserAgent.settings.defaultSites.map(extractDomains);

globalUnprotected.forEach((domain) => {
expect(defaultSites).to.contain(domain);
});
});

it('adds Android Chrome client-brand hints', () => {
const config = loadJSON('generated/v5/android-config.json');
const domains = config.features.clientBrandHint.settings.domains;

globalUnprotected.forEach((domain) => {
expect(domains).to.deep.include({ domain, brand: 'CHROME' });
});
});

it('adds Windows Chrome UA and client-hint mitigations', () => {
const config = loadJSON('generated/v5/windows-config.json');
const strategies = config.features.customUserAgent.features.userAgentStrategies.settings.strategies;
const clientHintDomains = config.features.clientBrandHint.settings.domains;
const uaChBrandDomains = config.features.uaChBrands.settings.conditionalChanges.map(
(change) => change.condition.domain,
);
const uaChBrandExceptions = config.features.uaChBrands.exceptions.map(extractDomains);

globalUnprotected.forEach((domain) => {
expect(strategies).to.deep.include({ strategy: 'ChromeUA', domain });
expect(clientHintDomains).to.deep.include({ domain, brand: 'Google Chrome' });
expect(uaChBrandDomains).to.contain(domain);
expect(uaChBrandExceptions).to.not.contain(domain);
});
});
});
});
96 changes: 96 additions & 0 deletions util.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,102 @@ export function mergeInterferenceTypes(base, override) {
return { ...base, ...override };
}

function getEntryDomain(entry) {
return typeof entry === 'string' ? entry : entry.domain;
}

function appendMissingDomainEntries(items, exceptions, createEntry) {
if (!Array.isArray(items)) {
return;
}

const domains = new Set(items.map(getEntryDomain));
for (const exception of exceptions) {
if (domains.has(exception.domain)) {
continue;
}
items.push(createEntry(exception));
domains.add(exception.domain);
}
}

function removeDomainEntries(items, exceptions) {
if (!Array.isArray(items)) {
return;
}

const domains = new Set(exceptions.map(({ domain }) => domain));
for (let i = items.length - 1; i >= 0; i--) {
if (domains.has(getEntryDomain(items[i]))) {
items.splice(i, 1);
}
}
}

function conditionMatchesDomain(condition, domain) {
if (Array.isArray(condition)) {
return condition.some((entry) => conditionMatchesDomain(entry, domain));
}
return condition?.domain === domain;
}

function addClientBrandHintDomains(config, exceptions, brand) {
const domains = config.features.clientBrandHint?.settings?.domains;
appendMissingDomainEntries(domains, exceptions, ({ domain }) => ({ domain, brand }));
}

function addWindowsChromeUserAgentStrategies(config, exceptions) {
const strategies = config.features.customUserAgent?.features?.userAgentStrategies?.settings?.strategies;
appendMissingDomainEntries(strategies, exceptions, ({ domain }) => ({ strategy: 'ChromeUA', domain }));
}

function addWindowsUaChBrands(config, exceptions) {
const feature = config.features.uaChBrands;
const conditionalChanges = feature?.settings?.conditionalChanges;
if (!Array.isArray(conditionalChanges)) {
return;
}

removeDomainEntries(feature.exceptions, exceptions);
for (const exception of exceptions) {
if (conditionalChanges.some((change) => conditionMatchesDomain(change.condition, exception.domain))) {
continue;
}
conditionalChanges.push({
condition: {
domain: exception.domain,
},
patchSettings: [
{
op: 'add',
path: '/brandName',
value: 'Google Chrome',
},
],
});
}
}

export function addUnprotectedTemporaryUserAgentMitigations(platform, config, exceptions) {
if (!exceptions.length) {
return;
}

const customUserAgentSettings = config.features.customUserAgent?.settings;
if (platform === 'ios') {
appendMissingDomainEntries(customUserAgentSettings?.ddgFixedSites, exceptions, (entry) => ({ ...entry }));
appendMissingDomainEntries(customUserAgentSettings?.omitApplicationSites, exceptions, (entry) => ({ ...entry }));
} else if (platform === 'macos') {
appendMissingDomainEntries(customUserAgentSettings?.defaultSites, exceptions, (entry) => ({ ...entry }));
} else if (platform === 'android') {
addClientBrandHintDomains(config, exceptions, 'CHROME');
} else if (platform === 'windows') {
addWindowsChromeUserAgentStrategies(config, exceptions);
addClientBrandHintDomains(config, exceptions, 'Google Chrome');
addWindowsUaChBrands(config, exceptions);
}
Comment on lines +231 to +242

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does seem correct.

Are you able to add an unprotected, temporary demo your domain to a follow-up PR that's based off this one, and create a draft PR, and then validate the output? (perhaps even in the client too)

}

/**
* Traverse the input (JSON data) and ensure any "reason" fields are strings in the output.
*
Expand Down
Loading