Skip to content
74 changes: 71 additions & 3 deletions src/extension/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,69 @@ import { ADMIN_ORIGIN, ADMIN_ORIGIN_NEW } from './utils/admin.js';
const { host: adminHost } = new URL(ADMIN_ORIGIN);
const { host: newAdminHost } = new URL(ADMIN_ORIGIN_NEW);

/** Cache-Control max-age in seconds for added project hosts (HTML, JSON and code): 1 minute. */
export const CACHE_MAX_AGE_SECONDS = 60;

function getRandomId() {
return Math.floor(Math.random() * 1000000);
}

/**
* Returns the hostname (domain) from a project host value (may be domain or full URL).
* @param {string} host
* @returns {string}
*/
export function getHostDomain(host) {
if (!host || typeof host !== 'string') return '';
return host.startsWith('http') ? new URL(host).host : host;
}

/**
* Builds declarativeNetRequest rules that set Cache-Control on responses
* for added project hosts.
* @param {Object[]} projectConfigs Configs with host, liveHost
* @returns {Object[]} Rules to add
*/
export function getCacheControlRules(projectConfigs) {
const rules = [];
projectConfigs
.flatMap((p) => [
p?.host,
p?.liveHost,
].filter(Boolean).map(getHostDomain))
.filter(Boolean)
.filter((domain, i, self) => self.indexOf(domain) === i)
.forEach((domain) => {
// @ts-ignore
const escaped = domain.replaceAll(/\./g, '\\.');
rules.push({
id: getRandomId(),
priority: 1,
action: {
type: 'modifyHeaders',
responseHeaders: [{
header: 'Cache-Control',
operation: 'set',
value: `max-age=${CACHE_MAX_AGE_SECONDS}, must-revalidate`,
}],
},
condition: {
regexFilter: `^https://${escaped}/.*`,
requestMethods: ['get'],
resourceTypes: [
'main_frame',
'sub_frame',
'script',
'stylesheet',
'xmlhttprequest',
'font',
],
},
});
});
return rules;
}

/**
* Sets the x-auth-token header for all requests to the Admin API if project config
* has an auth token. Also sets the Access-Control-Allow-Origin header for
Expand All @@ -36,6 +95,15 @@ export async function configureAuthAndCorsHeaders() {
removeRuleIds: (await chrome.declarativeNetRequest.getSessionRules())
.map((rule) => rule.id),
});
const allRules = [];

// Cache-Control rules for added project hosts (prod, preview, live, review) – override to 1 min
const syncHandles = await getConfig('sync', 'projects') || [];
const syncConfigs = (await Promise.all(
syncHandles.map((handle) => getConfig('sync', handle)),
)).filter(Boolean);
allRules.push(...getCacheControlRules(syncConfigs));

// find projects with auth tokens and add rules for each
const projects = await getConfig('session', 'projects') || [];
const addRulesPromises = projects.map(async ({
Expand Down Expand Up @@ -152,10 +220,10 @@ export async function configureAuthAndCorsHeaders() {
return rules;
});

const addRules = (await Promise.all(addRulesPromises)).flat();
if (addRules.length > 0) {
allRules.push(...(await Promise.all(addRulesPromises)).flat());
if (allRules.length > 0) {
await chrome.declarativeNetRequest.updateSessionRules({
addRules,
addRules: allRules,
});
}
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion src/extension/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from './config.js';
import { urlCache } from './url-cache.js';
import { callAdmin, createAdminUrl } from './utils/admin.js';
import { setAuthToken } from './auth.js';
import { configureAuthAndCorsHeaders, setAuthToken } from './auth.js';

export const DEV_URL = 'http://localhost:3000/';

Expand Down Expand Up @@ -78,6 +78,7 @@ export async function updateProject(project) {
projects.push(handle);
await setConfig('sync', { projects });
}
await configureAuthAndCorsHeaders();
log.info('updated project', project);
return project;
}
Expand Down
54 changes: 54 additions & 0 deletions test/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { setUserAgent } from '@web/test-runner-commands';
import sinon from 'sinon';

import {
CACHE_MAX_AGE_SECONDS,
configureAuthAndCorsHeaders,
getCacheControlRules,
getHostDomain,
setAuthToken,
updateUserAgent,
} from '../src/extension/auth.js';
Expand Down Expand Up @@ -54,6 +57,57 @@ describe('Test auth', () => {
await configureAuthAndCorsHeaders();
});

describe('getHostDomain', () => {
it('returns hostname for full URL', () => {
expect(getHostDomain('https://example.com/path')).to.equal('example.com');
expect(getHostDomain('https://sub.example.com:443/')).to.equal('sub.example.com');
});
it('returns input for plain domain', () => {
expect(getHostDomain('example.com')).to.equal('example.com');
expect(getHostDomain('main--repo--owner.aem.live')).to.equal('main--repo--owner.aem.live');
});
it('returns empty string for invalid or empty input', () => {
expect(getHostDomain('')).to.equal('');
expect(getHostDomain(null)).to.equal('');
expect(getHostDomain(undefined)).to.equal('');
expect(getHostDomain(123)).to.equal('');
});
});

describe('getCacheControlRules', () => {
it('returns one rule per unique host', () => {
const configs = [
{ host: 'prod.example.com' },
{ liveHost: 'live.example.com' },
];
const rules = getCacheControlRules(configs);
expect(rules).to.have.lengthOf(2);
expect(rules.every((r) => r.action?.responseHeaders?.[0]?.header === 'Cache-Control')).to.be.true;
expect(rules.every((r) => r.action.responseHeaders[0].value === `max-age=${CACHE_MAX_AGE_SECONDS}, must-revalidate`)).to.be.true;
const filters = rules.map((r) => r.condition.regexFilter);
expect(filters).to.include('^https://prod\\.example\\.com/.*');
expect(filters).to.include('^https://live\\.example\\.com/.*');
});
it('deduplicates same host across configs', () => {
const configs = [
{ host: 'same.com', liveHost: 'same.com' },
];
const rules = getCacheControlRules(configs);
expect(rules).to.have.lengthOf(1);
expect(rules[0].condition.regexFilter).to.equal('^https://same\\.com/.*');
});
it('extracts host from full URL in config', () => {
const configs = [{ host: 'https://url-host.com/path' }];
const rules = getCacheControlRules(configs);
expect(rules).to.have.lengthOf(1);
expect(rules[0].condition.regexFilter).to.equal('^https://url-host\\.com/.*');
});
it('returns empty array for empty or no valid hosts', () => {
expect(getCacheControlRules([])).to.deep.equal([]);
expect(getCacheControlRules([{ host: '' }, { liveHost: null }])).to.deep.equal([]);
});
});

it('setAuthToken', async () => {
const updateSessionRules = sandbox.spy(chrome.declarativeNetRequest, 'updateSessionRules');
const getConfig = sandbox.spy(chrome.storage.session, 'get');
Expand Down