fix(portal): preserve readable download filenames
This commit is contained in:
1 parent
596246f20a
commit
89cdb00765
5 files changed
+76
-45
No files matched your search
@@ -13,7 +13,9 @@ document without opening nginx's native Basic Auth dialog. Staff uses the
|
|||||||
stream `a`. Direct URLs remain nginx-protected, cross-role fetches are rejected
|
stream `a`. Direct URLs remain nginx-protected, cross-role fetches are rejected
|
||||||
before credentials are sent, and failed authenticated downloads show an inline
|
before credentials are sent, and failed authenticated downloads show an inline
|
||||||
error without direct-navigation fallback. No code enters a URL, browser
|
error without direct-navigation fallback. No code enters a URL, browser
|
||||||
storage, public artifact, or log.
|
storage, public artifact, or log. Downloads use a bounded safe form of the
|
||||||
|
approved link label and preserve the validated extension from the protected
|
||||||
|
SHA-256 server destination; server resources are not renamed.
|
||||||
|
|
||||||
## Approved production ciphertext
|
## Approved production ciphertext
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function safePortalContent(html: string): DocumentFragment {
|
|||||||
if (url.origin !== location.origin && url.protocol === 'https:') element.setAttribute('target', '_blank');
|
if (url.origin !== location.origin && url.protocol === 'https:') element.setAttribute('target', '_blank');
|
||||||
const presentation = portalLinkPresentation([...source.classList], source.style.backgroundColor, url.protocol, source.textContent ?? '');
|
const presentation = portalLinkPresentation([...source.classList], source.style.backgroundColor, url.protocol, source.textContent ?? '');
|
||||||
if (presentation) element.className = presentation;
|
if (presentation) element.className = presentation;
|
||||||
const resource = protectedPortalResource(url.href, location.origin);
|
const resource = protectedPortalResource(url.href, location.origin, source.textContent ?? '');
|
||||||
if (resource) element.dataset.portalResourceRole = resource.role;
|
if (resource) element.dataset.portalResourceRole = resource.role;
|
||||||
}
|
}
|
||||||
} catch { /* Invalid destination remains visible text without navigation. */ }
|
} catch { /* Invalid destination remains visible text without navigation. */ }
|
||||||
@@ -126,14 +126,16 @@ function initialize() {
|
|||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = objectUrl; link.download = filename; link.hidden = true;
|
link.href = objectUrl; link.download = filename; link.hidden = true;
|
||||||
document.body.append(link); link.click(); link.remove();
|
document.body.append(link); link.click(); link.remove();
|
||||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
// Both supported browsers have ample time to hand the Blob to their
|
||||||
|
// download manager before its temporary URL is released.
|
||||||
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
|
||||||
};
|
};
|
||||||
content.addEventListener('click', (event) => {
|
content.addEventListener('click', (event) => {
|
||||||
const target = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[data-portal-resource-role]') : null;
|
const target = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[data-portal-resource-role]') : null;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (downloading) return;
|
if (downloading) return;
|
||||||
const resource = protectedPortalResource(target.href, location.origin);
|
const resource = protectedPortalResource(target.href, location.origin, target.textContent ?? '');
|
||||||
if (!resource || !credential) {
|
if (!resource || !credential) {
|
||||||
downloadFeedback.textContent = portalDownloadErrorMessage(undefined);
|
downloadFeedback.textContent = portalDownloadErrorMessage(undefined);
|
||||||
downloadFeedback.hidden = false;
|
downloadFeedback.hidden = false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const roles = new Set(['staff', 'management']);
|
const roles = new Set(['staff', 'management']);
|
||||||
const resourcePath = /^\/(staff|management)-resources\/([A-Za-z0-9][A-Za-z0-9._-]{0,240})$/;
|
const resourcePath = /^\/(staff|management)-resources\/([A-Za-z0-9][A-Za-z0-9._-]{0,240})$/;
|
||||||
|
const serverExtension = /([.][a-z0-9]{1,10})$/i;
|
||||||
|
|
||||||
export function portalRoleForStreamId(id) {
|
export function portalRoleForStreamId(id) {
|
||||||
if (id === 'b') return 'staff';
|
if (id === 'b') return 'staff';
|
||||||
@@ -7,13 +8,32 @@ export function portalRoleForStreamId(id) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function protectedPortalResource(href, origin) {
|
export function recognizableDownloadFilename(label, serverFilename) {
|
||||||
|
const extension = serverExtension.exec(serverFilename)?.[1]?.toLowerCase();
|
||||||
|
if (!extension) return undefined;
|
||||||
|
let title = typeof label === 'string' ? label.trim() : '';
|
||||||
|
if (title.toLowerCase().endsWith(extension)) title = title.slice(0, -extension.length);
|
||||||
|
const cleaned = title
|
||||||
|
.normalize('NFKD')
|
||||||
|
.replace(/\p{M}/gu, '')
|
||||||
|
.replace(/[’']/g, '')
|
||||||
|
.replace(/&/g, ' and ')
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
const base = [...cleaned].slice(0, 180).join('').replace(/-+$/g, '');
|
||||||
|
if (!base || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(base)) return `AIA-document${extension}`;
|
||||||
|
return `${base}${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function protectedPortalResource(href, origin, label = '') {
|
||||||
let url;
|
let url;
|
||||||
try { url = new URL(href, origin); } catch { return undefined; }
|
try { url = new URL(href, origin); } catch { return undefined; }
|
||||||
if (url.origin !== origin || url.username || url.password || url.search || url.hash) return undefined;
|
if (url.origin !== origin || url.username || url.password || url.search || url.hash) return undefined;
|
||||||
const match = resourcePath.exec(url.pathname);
|
const match = resourcePath.exec(url.pathname);
|
||||||
if (!match) return undefined;
|
if (!match) return undefined;
|
||||||
return { role: match[1], filename: match[2], url: url.href };
|
const downloadFilename = recognizableDownloadFilename(label, match[2]);
|
||||||
|
if (!downloadFilename) return undefined;
|
||||||
|
return { role: match[1], filename: match[2], downloadFilename, url: url.href };
|
||||||
}
|
}
|
||||||
|
|
||||||
function basicAuthorization(role, code) {
|
function basicAuthorization(role, code) {
|
||||||
@@ -26,22 +46,6 @@ function basicAuthorization(role, code) {
|
|||||||
return `Basic ${btoa(binary)}`;
|
return `Basic ${btoa(binary)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeFilename(value, fallback) {
|
|
||||||
if (typeof value !== 'string') return fallback;
|
|
||||||
const name = value.trim();
|
|
||||||
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,240}$/.test(name) ? name : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function responseFilename(response, fallback) {
|
|
||||||
const disposition = response.headers.get('content-disposition') ?? '';
|
|
||||||
const encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
|
|
||||||
if (encoded) {
|
|
||||||
try { return safeFilename(decodeURIComponent(encoded), fallback); } catch { /* Use URL filename. */ }
|
|
||||||
}
|
|
||||||
const quoted = disposition.match(/filename="([^"]+)"/i)?.[1];
|
|
||||||
return safeFilename(quoted, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PortalDownloadError extends Error {
|
export class PortalDownloadError extends Error {
|
||||||
constructor(code) { super(code); this.name = 'PortalDownloadError'; this.code = code; }
|
constructor(code) { super(code); this.name = 'PortalDownloadError'; this.code = code; }
|
||||||
}
|
}
|
||||||
@@ -58,7 +62,7 @@ export async function fetchProtectedPortalResource(resource, credential, fetcher
|
|||||||
});
|
});
|
||||||
if (response.status === 401 || response.status === 403) throw new PortalDownloadError('unauthorized');
|
if (response.status === 401 || response.status === 403) throw new PortalDownloadError('unauthorized');
|
||||||
if (!response.ok) throw new PortalDownloadError('unavailable');
|
if (!response.ok) throw new PortalDownloadError('unavailable');
|
||||||
return { blob: await response.blob(), filename: responseFilename(response, resource.filename) };
|
return { blob: await response.blob(), filename: resource.downloadFilename };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function portalDownloadErrorMessage(error) {
|
export function portalDownloadErrorMessage(error) {
|
||||||
|
|||||||
@@ -74,11 +74,15 @@ try {
|
|||||||
const duration = 30 * 86400000;
|
const duration = 30 * 86400000;
|
||||||
const staffCode = randomBytes(24).toString('hex');
|
const staffCode = randomBytes(24).toString('hex');
|
||||||
const managementCode = randomBytes(24).toString('hex');
|
const managementCode = randomBytes(24).toString('hex');
|
||||||
|
const staffResourceName = 'a'.repeat(64) + '.pdf';
|
||||||
|
const managementResourceName = 'b'.repeat(64) + '.docx';
|
||||||
|
const staffResourcePath = `/staff-resources/${staffResourceName}`;
|
||||||
|
const managementResourcePath = `/management-resources/${managementResourceName}`;
|
||||||
const styledFixture = '<section><h2>Synthetic Directory</h2><h3>Operational reports</h3><p><a class="hs-button arbitrary-legacy-class" style="position:fixed" onclick="window.portalUnsafe=true" href="https://example.invalid/report">Synthetic report</a></p><ul><li>Synthetic list item</li></ul><table><caption>Fixture directory</caption><thead><tr><th scope="col">Role</th><th scope="col">Contact</th></tr></thead><tbody><tr><td>Fixture role</td><td><a href="mailto:fixture@example.invalid">Fixture email</a></td></tr></tbody></table><p><a style="background-color:#ffbd59" href="https://example.invalid/second">Second fixture report</a></p><p><a class="unrecognized-report-legacy" href="https://example.invalid/third">Operations Monthly Lineup Report - PM</a></p><a href="javascript:alert(1)">Unsafe destination</a><iframe src="https://example.invalid/frame"></iframe><form action="https://example.invalid/post"><input></form></section>';
|
const styledFixture = '<section><h2>Synthetic Directory</h2><h3>Operational reports</h3><p><a class="hs-button arbitrary-legacy-class" style="position:fixed" onclick="window.portalUnsafe=true" href="https://example.invalid/report">Synthetic report</a></p><ul><li>Synthetic list item</li></ul><table><caption>Fixture directory</caption><thead><tr><th scope="col">Role</th><th scope="col">Contact</th></tr></thead><tbody><tr><td>Fixture role</td><td><a href="mailto:fixture@example.invalid">Fixture email</a></td></tr></tbody></table><p><a style="background-color:#ffbd59" href="https://example.invalid/second">Second fixture report</a></p><p><a class="unrecognized-report-legacy" href="https://example.invalid/third">Operations Monthly Lineup Report - PM</a></p><a href="javascript:alert(1)">Unsafe destination</a><iframe src="https://example.invalid/frame"></iframe><form action="https://example.invalid/post"><input></form></section>';
|
||||||
const staffTemplates = '<section><h2>Management Templates</h2><ul><li><div><a href="https://example.invalid/check-in">Client Check-In & Check-Out</a></div></li><li><div><a href="https://example.invalid/employee-performance">Employee Performance Template</a></div></li><li><div><a href="https://example.invalid/employee-availability">Employee Availability Template</a></div></li></ul></section><section><h2>Other Staff Resources</h2><p><a href="https://example.invalid/resource">General staff resource</a></p><p><a href="mailto:staff@example.invalid">Staff email</a></p></section>';
|
const staffTemplates = '<section><h2>Management Templates</h2><ul><li><div><a href="https://example.invalid/check-in">Client Check-In & Check-Out</a></div></li><li><div><a href="https://example.invalid/employee-performance">Employee Performance Template</a></div></li><li><div><a href="https://example.invalid/employee-availability">Employee Availability Template</a></div></li></ul></section><section><h2>Other Staff Resources</h2><p><a href="https://example.invalid/resource">General staff resource</a></p><p><a href="mailto:staff@example.invalid">Staff email</a></p></section>';
|
||||||
const payload = { alg: portalAlgorithm, streams: [
|
const payload = { alg: portalAlgorithm, streams: [
|
||||||
await encryptPortalStream('b', '<h2>Synthetic Staff</h2><p>Staff fixture only</p><a href="/staff-resources/staff-guide.pdf">Download staff guide</a><script>window.portalUnsafe=true</script><img src="https://example.invalid/image">' + styledFixture + staffTemplates, staffCode),
|
await encryptPortalStream('b', `<h2>Synthetic Staff</h2><p>Staff fixture only</p><a href="${staffResourcePath}">Staff's Safety & Training: Résumé (2026)!</a><script>window.portalUnsafe=true</script><img src="https://example.invalid/image">` + styledFixture + staffTemplates, staffCode),
|
||||||
await encryptPortalStream('a', '<h2>Synthetic Management</h2><p>Management fixture only</p><a class="hs-button" href="/management-resources/management-guide.pdf">Download management guide</a>' + styledFixture, managementCode),
|
await encryptPortalStream('a', `<h2>Synthetic Management</h2><p>Management fixture only</p><a class="hs-button" href="${managementResourcePath}">Management – Policies / Q&A #1</a>` + styledFixture, managementCode),
|
||||||
] };
|
] };
|
||||||
const expectedAuthorization = {
|
const expectedAuthorization = {
|
||||||
staff: 'Basic ' + Buffer.from('staff:' + staffCode, 'utf8').toString('base64'),
|
staff: 'Basic ' + Buffer.from('staff:' + staffCode, 'utf8').toString('base64'),
|
||||||
@@ -89,7 +93,7 @@ try {
|
|||||||
await context.route('**/*', async (route) => {
|
await context.route('**/*', async (route) => {
|
||||||
const req = route.request(), url = new URL(req.url());
|
const req = route.request(), url = new URL(req.url());
|
||||||
if (url.origin === origin && ['GET', 'HEAD'].includes(req.method())) {
|
if (url.origin === origin && ['GET', 'HEAD'].includes(req.method())) {
|
||||||
const resource = /^\/(staff|management)-resources\/(staff|management)-guide\.pdf$/.exec(url.pathname);
|
const resource = /^\/(staff|management)-resources\/([a-f0-9]{64})[.](pdf|docx)$/.exec(url.pathname);
|
||||||
if (resource) {
|
if (resource) {
|
||||||
const role = resource[1];
|
const role = resource[1];
|
||||||
const authorized = req.headers().authorization === expectedAuthorization[role];
|
const authorized = req.headers().authorization === expectedAuthorization[role];
|
||||||
@@ -97,9 +101,9 @@ try {
|
|||||||
if (!authorized) return route.fulfill({ status: 401, headers: { 'www-authenticate': `Basic realm="AIA ${role} resources"`, 'cache-control': 'private, no-store' } });
|
if (!authorized) return route.fulfill({ status: 401, headers: { 'www-authenticate': `Basic realm="AIA ${role} resources"`, 'cache-control': 'private, no-store' } });
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/pdf',
|
contentType: resource[3] === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
headers: { 'content-disposition': `attachment; filename="${role}-guide.pdf"`, 'cache-control': 'private, no-store' },
|
headers: { 'cache-control': 'private, no-store' },
|
||||||
body: `%PDF-1.7\nSynthetic ${role} document\n`,
|
body: resource[3] === 'pdf' ? `%PDF-1.7\nSynthetic ${role} document\n` : `Synthetic ${role} document\n`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (inject && url.pathname === '/employee-portal') {
|
if (inject && url.pathname === '/employee-portal') {
|
||||||
@@ -382,15 +386,21 @@ try {
|
|||||||
assert.equal(columns, width >= 768 ? 2 : 1, 'staff templates follow responsive column count');
|
assert.equal(columns, width >= 768 ? 2 : 1, 'staff templates follow responsive column count');
|
||||||
assert.equal(await staffContent.getByRole('link', { name: 'General staff resource' }).getAttribute('class'), 'portal-resource-button');
|
assert.equal(await staffContent.getByRole('link', { name: 'General staff resource' }).getAttribute('class'), 'portal-resource-button');
|
||||||
assert.equal(await staffContent.getByRole('link', { name: 'Staff email' }).getAttribute('class'), 'portal-contact-link');
|
assert.equal(await staffContent.getByRole('link', { name: 'Staff email' }).getAttribute('class'), 'portal-contact-link');
|
||||||
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + '/staff-resources/staff-guide.pdf'), 401);
|
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + staffResourcePath), 401);
|
||||||
const staffLink = staffContent.getByRole('link', { name: 'Download staff guide' });
|
await p.evaluate(() => {
|
||||||
|
window.portalObjectUrlRevocations = [];
|
||||||
|
const revoke = URL.revokeObjectURL.bind(URL);
|
||||||
|
URL.revokeObjectURL = (url) => { window.portalObjectUrlRevocations.push(url); revoke(url); };
|
||||||
|
});
|
||||||
|
const staffLink = staffContent.getByRole('link', { name: "Staff's Safety & Training: Résumé (2026)!" });
|
||||||
const staffDownload = p.waitForEvent('download');
|
const staffDownload = p.waitForEvent('download');
|
||||||
await staffLink.click();
|
await staffLink.click();
|
||||||
assert.equal((await staffDownload).suggestedFilename(), 'staff-guide.pdf');
|
assert.equal((await staffDownload).suggestedFilename(), 'Staffs-Safety-and-Training-Resume-2026.pdf');
|
||||||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded staff-guide.pdf.');
|
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded Staffs-Safety-and-Training-Resume-2026.pdf.');
|
||||||
|
assert.equal(await p.evaluate(() => window.portalObjectUrlRevocations.length), 0, 'Blob URL remains valid while the browser begins the download');
|
||||||
assert.equal(p.url(), origin + '/employee-portal');
|
assert.equal(p.url(), origin + '/employee-portal');
|
||||||
const beforeCrossRole = evidence.resourceRequests.length;
|
const beforeCrossRole = evidence.resourceRequests.length;
|
||||||
await staffLink.evaluate((link) => { link.href = '/management-resources/management-guide.pdf'; });
|
await staffLink.evaluate((link, href) => { link.href = href; }, managementResourcePath);
|
||||||
await staffLink.click();
|
await staffLink.click();
|
||||||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent?.includes('not available from this portal'));
|
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent?.includes('not available from this portal'));
|
||||||
assert.equal(evidence.resourceRequests.length, beforeCrossRole, 'staff credential is not sent to management resources');
|
assert.equal(evidence.resourceRequests.length, beforeCrossRole, 'staff credential is not sent to management resources');
|
||||||
@@ -405,14 +415,20 @@ try {
|
|||||||
assert.match(await p.locator('[data-portal-content]').textContent(), /Synthetic Management/);
|
assert.match(await p.locator('[data-portal-content]').textContent(), /Synthetic Management/);
|
||||||
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Staff fixture/);
|
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Staff fixture/);
|
||||||
await checkPresentation();
|
await checkPresentation();
|
||||||
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + '/management-resources/management-guide.pdf'), 401);
|
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + managementResourcePath), 401);
|
||||||
const managementLink = p.getByRole('link', { name: 'Download management guide' });
|
await p.evaluate(() => {
|
||||||
|
window.portalObjectUrlRevocations = [];
|
||||||
|
const revoke = URL.revokeObjectURL.bind(URL);
|
||||||
|
URL.revokeObjectURL = (url) => { window.portalObjectUrlRevocations.push(url); revoke(url); };
|
||||||
|
});
|
||||||
|
const managementLink = p.getByRole('link', { name: 'Management – Policies / Q&A #1' });
|
||||||
const managementDownload = p.waitForEvent('download');
|
const managementDownload = p.waitForEvent('download');
|
||||||
await managementLink.click();
|
await managementLink.click();
|
||||||
assert.equal((await managementDownload).suggestedFilename(), 'management-guide.pdf');
|
assert.equal((await managementDownload).suggestedFilename(), 'Management-Policies-Q-and-A-1.docx');
|
||||||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded management-guide.pdf.');
|
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded Management-Policies-Q-and-A-1.docx.');
|
||||||
|
assert.equal(await p.evaluate(() => window.portalObjectUrlRevocations.length), 0, 'Blob URL remains valid while the browser begins the download');
|
||||||
const beforeManagementCrossRole = evidence.resourceRequests.length;
|
const beforeManagementCrossRole = evidence.resourceRequests.length;
|
||||||
await managementLink.evaluate((link) => { link.href = '/staff-resources/staff-guide.pdf'; });
|
await managementLink.evaluate((link, href) => { link.href = href; }, staffResourcePath);
|
||||||
await managementLink.click();
|
await managementLink.click();
|
||||||
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent?.includes('not available from this portal'));
|
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent?.includes('not available from this portal'));
|
||||||
assert.equal(evidence.resourceRequests.length, beforeManagementCrossRole, 'management credential is not sent to staff resources');
|
assert.equal(evidence.resourceRequests.length, beforeManagementCrossRole, 'management credential is not sent to staff resources');
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
portalDownloadErrorMessage,
|
portalDownloadErrorMessage,
|
||||||
portalRoleForStreamId,
|
portalRoleForStreamId,
|
||||||
protectedPortalResource,
|
protectedPortalResource,
|
||||||
|
recognizableDownloadFilename,
|
||||||
} from '../src/utils/portal-download.mjs';
|
} from '../src/utils/portal-download.mjs';
|
||||||
import { applyRoleMappings, approvedMapping, summarizeMappings } from './portal-production.mjs';
|
import { applyRoleMappings, approvedMapping, summarizeMappings } from './portal-production.mjs';
|
||||||
|
|
||||||
@@ -60,12 +61,17 @@ test('portal downloads use only the validated role and an in-memory authorizatio
|
|||||||
assert.equal(portalRoleForStreamId('a'), 'management');
|
assert.equal(portalRoleForStreamId('a'), 'management');
|
||||||
assert.equal(portalRoleForStreamId('fixture-staff'), undefined);
|
assert.equal(portalRoleForStreamId('fixture-staff'), undefined);
|
||||||
const origin = 'https://www.azinstitute4autism.com';
|
const origin = 'https://www.azinstitute4autism.com';
|
||||||
const staff = protectedPortalResource('/staff-resources/staff-guide.pdf', origin);
|
const staff = protectedPortalResource('/staff-resources/' + 'a'.repeat(64) + '.pdf', origin, "Staff's Safety & Training: Résumé (2026)!");
|
||||||
const management = protectedPortalResource('/management-resources/management-guide.pdf', origin);
|
const management = protectedPortalResource('/management-resources/' + 'b'.repeat(64) + '.docx', origin, 'Management – Policies / Q&A #1');
|
||||||
assert.deepEqual(staff, {
|
assert.deepEqual(staff, {
|
||||||
role: 'staff', filename: 'staff-guide.pdf',
|
role: 'staff', filename: 'a'.repeat(64) + '.pdf',
|
||||||
url: origin + '/staff-resources/staff-guide.pdf',
|
downloadFilename: 'Staffs-Safety-and-Training-Resume-2026.pdf',
|
||||||
|
url: origin + '/staff-resources/' + 'a'.repeat(64) + '.pdf',
|
||||||
});
|
});
|
||||||
|
assert.equal(management.downloadFilename, 'Management-Policies-Q-and-A-1.docx');
|
||||||
|
assert.equal(recognizableDownloadFilename('Client Check-In & Check-Out.xlsx', 'c'.repeat(64) + '.xlsx'), 'Client-Check-In-and-Check-Out.xlsx');
|
||||||
|
assert.equal(recognizableDownloadFilename('../../', 'd'.repeat(64) + '.pdf'), 'AIA-document.pdf');
|
||||||
|
assert.equal(recognizableDownloadFilename('No server extension', 'e'.repeat(64)), undefined);
|
||||||
assert.equal(protectedPortalResource('/staff-resources/nested/file.pdf', origin), undefined);
|
assert.equal(protectedPortalResource('/staff-resources/nested/file.pdf', origin), undefined);
|
||||||
assert.equal(protectedPortalResource('/staff-resources/file.pdf?code=forbidden', origin), undefined);
|
assert.equal(protectedPortalResource('/staff-resources/file.pdf?code=forbidden', origin), undefined);
|
||||||
assert.equal(protectedPortalResource('https://other.invalid/staff-resources/file.pdf', origin), undefined);
|
assert.equal(protectedPortalResource('https://other.invalid/staff-resources/file.pdf', origin), undefined);
|
||||||
@@ -75,7 +81,8 @@ test('portal downloads use only the validated role and an in-memory authorizatio
|
|||||||
const result = await fetchProtectedPortalResource(staff, { role: 'staff', code }, async (url, options) => {
|
const result = await fetchProtectedPortalResource(staff, { role: 'staff', code }, async (url, options) => {
|
||||||
captured = { url, options };
|
captured = { url, options };
|
||||||
return new Response('synthetic document', {
|
return new Response('synthetic document', {
|
||||||
headers: { 'content-disposition': 'attachment; filename="approved-name.pdf"' },
|
// nginx may expose the hash name; the approved label remains authoritative.
|
||||||
|
headers: { 'content-disposition': `attachment; filename="${staff.filename}"` },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
assert.equal(captured.url, staff.url);
|
assert.equal(captured.url, staff.url);
|
||||||
@@ -86,7 +93,7 @@ test('portal downloads use only the validated role and an in-memory authorizatio
|
|||||||
assert.equal(captured.options.redirect, 'error');
|
assert.equal(captured.options.redirect, 'error');
|
||||||
assert.equal(captured.options.referrerPolicy, 'no-referrer');
|
assert.equal(captured.options.referrerPolicy, 'no-referrer');
|
||||||
assert.match(captured.options.headers.Authorization, /^Basic /);
|
assert.match(captured.options.headers.Authorization, /^Basic /);
|
||||||
assert.equal(result.filename, 'approved-name.pdf');
|
assert.equal(result.filename, 'Staffs-Safety-and-Training-Resume-2026.pdf');
|
||||||
assert.equal(await result.blob.text(), 'synthetic document');
|
assert.equal(await result.blob.text(), 'synthetic document');
|
||||||
|
|
||||||
for (const [resource, credential] of [
|
for (const [resource, credential] of [
|
||||||
|
|||||||
Reference in new issue
Block a user