feat(portal): reuse unlock code for protected downloads

This commit is contained in:
DeCentN2Madness authored and AIAMarketing committed 2026-11-05 19:21:26 -07:00
1 parent 475dd3379b
commit 596246f20a
9 files changed
+268 -13

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@
packages = developmentPackages ++ [ pkgs.playwright-driver ];
PLAYWRIGHT_MODULE = "${pkgs.playwright-driver}/index.mjs";
PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.selectBrowsers {
withFirefox = false;
withFirefox = true;
withWebkit = false;
withFfmpeg = false;
};
+7 -1
View File
@@ -619,7 +619,13 @@ dry-run, rollback, the encrypted production portal input and the separate
one-time employee-resource installer. The server-side installer is a root-only
POSIX shell script using NixOS base tools; it does not require Node.js on
Contabo. Ordinary releases never copy employee documents into `dist`;
protected staff and management resources remain under server-controlled Basic Auth namespaces. The accepted interim portal is
protected staff and management resources remain under server-controlled Basic
Auth namespaces. After a successful unlock, the page keeps that role's code
only in memory and uses it for same-origin authenticated document fetches. The
response is downloaded as a local Blob, so employees do not receive a second
browser authentication prompt. Cross-role requests are stopped before
credentials are sent; direct resource URLs still require nginx authentication.
Codes are never put in URLs or browser storage. The accepted interim portal is
explicitly non-PHI.
### Google Ads consultation-form conversion handoff
@@ -5,6 +5,16 @@ staff and management portal content and the interim shared-code presentation.
Browser-side decryption remains access friction rather than individual
authentication, revocation, a secure session, or a HIPAA determination.
The same successfully validated code authorizes protected document downloads
for that role during the current page session. The browser keeps it only in a
closure, performs a same-origin authenticated fetch, and saves the returned
document without opening nginx's native Basic Auth dialog. Staff uses the
`staff` username and stream `b`; management uses the `management` username and
stream `a`. Direct URLs remain nginx-protected, cross-role fetches are rejected
before credentials are sent, and failed authenticated downloads show an inline
error without direct-navigation fallback. No code enters a URL, browser
storage, public artifact, or log.
## Approved production ciphertext
An indexing-enabled production build may include one owner-approved encrypted
+10
View File
@@ -92,3 +92,13 @@ resource namespaces, denial cases, redirects, Form A, IPv4 and IPv6. Portal
shared-code rotation is coordinated: update VPS SOPS inputs and privately
re-encrypt the browser payload together. This interim portal is non-PHI and is
not an employee identity system.
The portal uses one code entry per page session. A successful decrypt retains
the code only in page memory and sends the matching fixed username (`staff` or
`management`) in an explicit same-origin Basic Authorization header when a
protected link is clicked. It downloads the authenticated response as a Blob
and never navigates directly to the protected URL after an error. Verify that
ordinary clicks download without a native credential dialog, inline errors
appear for denied or unavailable files, direct unauthenticated requests remain
401, and neither role can request the other namespace. Reloading, leaving the
page, or an Astro page swap discards the in-memory credential.
@@ -25,6 +25,7 @@ const serialized = payload ? JSON.stringify(payload).replaceAll('<', '\\u003c')
<noscript><p>JavaScript is required to unlock the employee portal locally. Please contact AIA for access assistance.</p></noscript>
</div>
<div class="employee-portal__content" tabindex="-1" role="region" aria-label="Unlocked employee portal" hidden data-portal-content></div>
<p class="employee-portal__download-feedback" role="status" aria-live="polite" hidden data-portal-download-feedback></p>
<script is:inline type="application/json" data-portal-payload set:html={serialized}></script>
</section>
@@ -35,6 +36,7 @@ const serialized = payload ? JSON.stringify(payload).replaceAll('<', '\\u003c')
input { background: #fcf9f5; border: 1px solid #edeef3; border-radius: 4px; font: inherit; padding: .65rem; max-width: 100%; min-width: 0; }
input:focus-visible { outline: 3px solid var(--color-focus, #115bbd); outline-offset: 3px; }
.employee-portal__content:focus { outline: none; }
.employee-portal__download-feedback { margin-block: 1rem; font-weight: 600; }
[hidden] { display: none; }
.employee-portal__content :global(a) { color: var(--color-primary); }
.employee-portal__content :global(a:focus-visible) { outline: 3px solid var(--color-focus, #115bbd); outline-offset: 3px; }
+52 -2
View File
@@ -1,4 +1,5 @@
import { decryptPortalStream, validatePortalPayload } from '../utils/portal-crypto.mjs';
import { fetchProtectedPortalResource, portalDownloadErrorMessage, portalRoleForStreamId, protectedPortalResource } from '../utils/portal-download.mjs';
import { portalLinkPresentation } from '../utils/portal-presentation';
/** Build a passive DOM, never inject executable decrypted HTML. Remote media,
@@ -24,6 +25,8 @@ export function safePortalContent(html: string): DocumentFragment {
if (url.origin !== location.origin && url.protocol === 'https:') element.setAttribute('target', '_blank');
const presentation = portalLinkPresentation([...source.classList], source.style.backgroundColor, url.protocol, source.textContent ?? '');
if (presentation) element.className = presentation;
const resource = protectedPortalResource(url.href, location.origin);
if (resource) element.dataset.portalResourceRole = resource.role;
}
} catch { /* Invalid destination remains visible text without navigation. */ }
}
@@ -99,6 +102,9 @@ function initialize() {
const feedback = root.querySelector<HTMLElement>('[data-portal-feedback]')!;
const prompt = root.querySelector<HTMLElement>('[data-portal-prompt]')!;
const content = root.querySelector<HTMLElement>('[data-portal-content]')!;
const downloadFeedback = root.querySelector<HTMLElement>('[data-portal-download-feedback]')!;
let credential: { role: 'staff' | 'management'; code: string } | null = null;
let downloading = false;
let data: ReturnType<typeof validatePortalPayload>;
try {
if (!crypto.subtle) throw new Error();
@@ -106,6 +112,45 @@ function initialize() {
input.disabled = false; button.disabled = false;
feedback.textContent = '';
} catch { feedback.textContent = 'Portal access is unavailable in this preview. Please contact AIA.'; return; }
const clearSession = () => {
credential = null;
input.value = '';
content.replaceChildren();
content.hidden = true;
prompt.hidden = false;
downloadFeedback.hidden = true;
downloadFeedback.textContent = '';
};
const saveDownload = (blob: Blob, filename: string) => {
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl; link.download = filename; link.hidden = true;
document.body.append(link); link.click(); link.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
};
content.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[data-portal-resource-role]') : null;
if (!target) return;
event.preventDefault();
if (downloading) return;
const resource = protectedPortalResource(target.href, location.origin);
if (!resource || !credential) {
downloadFeedback.textContent = portalDownloadErrorMessage(undefined);
downloadFeedback.hidden = false;
return;
}
downloading = true;
target.setAttribute('aria-disabled', 'true');
downloadFeedback.textContent = 'Downloading document…';
downloadFeedback.hidden = false;
void fetchProtectedPortalResource(resource, credential)
.then(({ blob, filename }) => {
saveDownload(blob, filename);
downloadFeedback.textContent = `Downloaded ${filename}.`;
})
.catch((error) => { downloadFeedback.textContent = portalDownloadErrorMessage(error); })
.finally(() => { downloading = false; target.removeAttribute('aria-disabled'); });
});
const unlock = async () => {
if (button.disabled) return;
button.disabled = true;
@@ -116,8 +161,12 @@ function initialize() {
for (const stream of data.streams) {
const plain = await decryptPortalStream(stream, pass);
if (plain !== null) {
const role = portalRoleForStreamId(stream.id);
if (!role) throw new Error('Unknown portal stream');
credential = { role, code: pass };
content.replaceChildren(safePortalContent(plain));
prompt.hidden = true; content.hidden = false;
downloadFeedback.hidden = true; downloadFeedback.textContent = '';
input.value = ''; content.focus(); return;
}
}
@@ -127,8 +176,9 @@ function initialize() {
};
button.addEventListener('click', unlock);
input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); void unlock(); } });
// BFCache must not restore an already unlocked view.
addEventListener('pagehide', () => { input.value = ''; content.replaceChildren(); content.hidden = true; prompt.hidden = false; });
// Credentials exist only in this page closure and are discarded on exit/swap.
addEventListener('pagehide', clearSession, { once: true });
document.addEventListener('astro:before-swap', clearSession, { once: true });
}
initialize();
document.addEventListener('astro:page-load', initialize);
+70
View File
@@ -0,0 +1,70 @@
const roles = new Set(['staff', 'management']);
const resourcePath = /^\/(staff|management)-resources\/([A-Za-z0-9][A-Za-z0-9._-]{0,240})$/;
export function portalRoleForStreamId(id) {
if (id === 'b') return 'staff';
if (id === 'a') return 'management';
return undefined;
}
export function protectedPortalResource(href, origin) {
let url;
try { url = new URL(href, origin); } catch { return undefined; }
if (url.origin !== origin || url.username || url.password || url.search || url.hash) return undefined;
const match = resourcePath.exec(url.pathname);
if (!match) return undefined;
return { role: match[1], filename: match[2], url: url.href };
}
function basicAuthorization(role, code) {
if (!roles.has(role) || typeof code !== 'string' || !code || code.length > 1024 || /[\r\n\0]/.test(code)) {
throw new PortalDownloadError('session');
}
const bytes = new TextEncoder().encode(`${role}:${code}`);
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
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 {
constructor(code) { super(code); this.name = 'PortalDownloadError'; this.code = code; }
}
export async function fetchProtectedPortalResource(resource, credential, fetcher = fetch) {
if (!resource || !credential || resource.role !== credential.role) throw new PortalDownloadError('role');
const response = await fetcher(resource.url, {
method: 'GET',
headers: { Authorization: basicAuthorization(credential.role, credential.code) },
credentials: 'omit',
cache: 'no-store',
redirect: 'error',
referrerPolicy: 'no-referrer',
});
if (response.status === 401 || response.status === 403) throw new PortalDownloadError('unauthorized');
if (!response.ok) throw new PortalDownloadError('unavailable');
return { blob: await response.blob(), filename: responseFilename(response, resource.filename) };
}
export function portalDownloadErrorMessage(error) {
if (error instanceof PortalDownloadError && error.code === 'role') return 'This document is not available from this portal.';
if (error instanceof PortalDownloadError && (error.code === 'unauthorized' || error.code === 'session')) {
return 'Your portal session could not authorize this download. Reload the page and enter the access code again.';
}
return 'The document could not be downloaded. Please try again or contact AIA.';
}
+56 -9
View File
@@ -16,6 +16,8 @@ import { setTimeout as delay } from 'node:timers/promises';
const forms = process.argv.find(a => a.startsWith('--forms='))?.split('=')[1] ?? 'live';
assert.ok(['live', 'off', 'mock'].includes(forms));
const browserName = process.argv.find(a => a.startsWith('--browser='))?.split('=')[1] ?? 'chromium';
assert.ok(['chromium', 'firefox'].includes(browserName));
const root = fileURLToPath(new URL('../', import.meta.url));
const moduleName = process.env.PLAYWRIGHT_MODULE
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
@@ -33,7 +35,7 @@ const routes = [
{ route: '/es/learner-social-club', locale: 'es', drawer: true, screenshot: 'es-learner-social-club' },
];
const widths = [320, 390, 768, 1440];
const evidence = { forms, routes, widths, checks: 0, blocked: [], jotformAttempts: [], screenshots: [], fallbackChecks: [], status: 'failed' };
const evidence = { forms, browserName, routes, widths, checks: 0, blocked: [], jotformAttempts: [], resourceRequests: [], screenshots: [], fallbackChecks: [], status: 'failed' };
const reserve = net.createServer();
await new Promise((resolve, reject) => { reserve.once('error', reject); reserve.listen(0, '127.0.0.1', resolve); });
const port = reserve.address().port;
@@ -47,9 +49,11 @@ function expectedUrl(locale) {
}
try {
const { chromium } = await import(moduleName);
browser = await chromium.launch(process.env.CHROMIUM_EXECUTABLE
? { headless: true, executablePath: process.env.CHROMIUM_EXECUTABLE }
const drivers = await import(moduleName);
const driver = drivers[browserName];
const executablePath = browserName === 'chromium' ? process.env.CHROMIUM_EXECUTABLE : process.env.FIREFOX_EXECUTABLE;
browser = await driver.launch(executablePath
? { headless: true, executablePath }
: { headless: true });
evidence.browser = browser.version();
preview = spawn(process.execPath, ['node_modules/astro/bin/astro.mjs', 'preview', '--host', '127.0.0.1', '--port', String(port)], {
@@ -73,14 +77,31 @@ try {
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 &amp; 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: [
await encryptPortalStream('fixture-staff', '<h2>Synthetic Staff</h2><p>Staff fixture only</p><a href="https://example.invalid/document">Synthetic resource</a><script>window.portalUnsafe=true</script><img src="https://example.invalid/image">' + styledFixture + staffTemplates, staffCode),
await encryptPortalStream('fixture-management', '<h2>Synthetic Management</h2><p>Management fixture only</p>' + styledFixture, managementCode),
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('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),
] };
const expectedAuthorization = {
staff: 'Basic ' + Buffer.from('staff:' + staffCode, 'utf8').toString('base64'),
management: 'Basic ' + Buffer.from('management:' + managementCode, 'utf8').toString('base64'),
};
const guard = async (context, inject = false) => {
await context.routeWebSocket('**/*', (socket) => { evidence.blocked.push({ method: 'WEBSOCKET' }); socket.close(); });
await context.route('**/*', async (route) => {
const req = route.request(), url = new URL(req.url());
if (url.origin === origin && ['GET', 'HEAD'].includes(req.method())) {
const resource = /^\/(staff|management)-resources\/(staff|management)-guide\.pdf$/.exec(url.pathname);
if (resource) {
const role = resource[1];
const authorized = req.headers().authorization === expectedAuthorization[role];
evidence.resourceRequests.push({ role, authorized });
if (!authorized) return route.fulfill({ status: 401, headers: { 'www-authenticate': `Basic realm="AIA ${role} resources"`, 'cache-control': 'private, no-store' } });
return route.fulfill({
status: 200,
contentType: 'application/pdf',
headers: { 'content-disposition': `attachment; filename="${role}-guide.pdf"`, 'cache-control': 'private, no-store' },
body: `%PDF-1.7\nSynthetic ${role} document\n`,
});
}
if (inject && url.pathname === '/employee-portal') {
const response = await route.fetch();
const html = (await response.text()).replace(/(<script[^>]*data-portal-payload[^>]*>)[\s\S]*?(<\/script>)/, '$1' + JSON.stringify(payload) + '$2');
@@ -327,14 +348,14 @@ try {
await input.fill(staffCode); await input.press('Enter');
await p.locator('[data-portal-content]').waitFor({ state: 'visible' });
assert.match(await p.locator('[data-portal-content]').textContent(), /Synthetic Staff/);
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Management/);
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Synthetic Management|Management fixture only/);
assert.equal(await p.evaluate(() => window.portalUnsafe), undefined);
assert.equal(await p.locator('[data-portal-content] img, [data-portal-content] script').count(), 0);
assert.equal(await p.evaluate(() => document.activeElement?.hasAttribute('data-portal-content')), true);
assert.equal(await input.inputValue(), '');
const checkPresentation = async (staff = false) => {
const content = p.locator('[data-portal-content]');
assert.equal(await content.locator('.portal-resource-button').count(), staff ? 8 : 3);
assert.equal(await content.locator('.portal-resource-button').count(), staff ? 8 : 4);
assert.equal(await content.locator('[style], [onclick], .arbitrary-legacy-class, .unrecognized-report-legacy, iframe, form, input').count(), 0);
assert.equal(await content.locator('a[href^="javascript:"]').count(), 0);
assert.equal(await content.locator('th[scope="col"]').count(), 2);
@@ -346,7 +367,7 @@ try {
assert.equal(await content.locator('li').first().evaluate(e => getComputedStyle(e, '::marker').color), 'rgb(255, 189, 89)');
assert.equal(await content.locator('h2').first().evaluate(e => getComputedStyle(e).color), 'rgb(37, 64, 128)');
assert.ok(await content.locator('h2').first().evaluate(e => parseFloat(getComputedStyle(e).fontSize) >= 28));
assert.equal(await content.locator('.portal-contact-link').evaluate(e => getComputedStyle(e).color), 'rgb(169, 70, 0)');
assert.equal(await content.locator('.portal-contact-link').first().evaluate(e => getComputedStyle(e).color), 'rgb(169, 70, 0)');
await button.focus();
assert.equal(await button.evaluate(e => getComputedStyle(e).outlineStyle), 'solid');
assert.equal(await button.evaluate(e => getComputedStyle(e).outlineWidth), '3px');
@@ -361,6 +382,20 @@ try {
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: '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);
const staffLink = staffContent.getByRole('link', { name: 'Download staff guide' });
const staffDownload = p.waitForEvent('download');
await staffLink.click();
assert.equal((await staffDownload).suggestedFilename(), 'staff-guide.pdf');
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded staff-guide.pdf.');
assert.equal(p.url(), origin + '/employee-portal');
const beforeCrossRole = evidence.resourceRequests.length;
await staffLink.evaluate((link) => { link.href = '/management-resources/management-guide.pdf'; });
await staffLink.click();
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.deepEqual(await p.evaluate(() => ({ local: { ...localStorage }, session: { ...sessionStorage } })), { local: {}, session: {} });
assert.equal(p.url().includes(staffCode), false);
assert.ok(await p.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await p.screenshot({ path: path.join(output, 'portal-synthetic-' + width + '.png'), fullPage: true });
evidence.screenshots.push('portal-synthetic-' + width + '.png');
@@ -370,6 +405,18 @@ try {
assert.match(await p.locator('[data-portal-content]').textContent(), /Synthetic Management/);
assert.doesNotMatch(await p.locator('[data-portal-content]').textContent(), /Staff fixture/);
await checkPresentation();
assert.equal(await p.evaluate(async (url) => (await fetch(url)).status, origin + '/management-resources/management-guide.pdf'), 401);
const managementLink = p.getByRole('link', { name: 'Download management guide' });
const managementDownload = p.waitForEvent('download');
await managementLink.click();
assert.equal((await managementDownload).suggestedFilename(), 'management-guide.pdf');
await p.waitForFunction(() => document.querySelector('[data-portal-download-feedback]')?.textContent === 'Downloaded management-guide.pdf.');
const beforeManagementCrossRole = evidence.resourceRequests.length;
await managementLink.evaluate((link) => { link.href = '/staff-resources/staff-guide.pdf'; });
await managementLink.click();
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(p.url().includes(managementCode), false);
assert.equal(await p.locator('[data-portal-content] ul.portal-template-grid').count(), 0, 'management view retains original layout');
await p.screenshot({ path: path.join(output, 'portal-management-synthetic-' + width + '.png'), fullPage: true });
evidence.screenshots.push('portal-management-synthetic-' + width + '.png');
+60
View File
@@ -10,6 +10,13 @@ import { decryptPortalStream, encryptPortalStream, validatePortalPayload, portal
import { collectResource, resourceReferences, rewriteResources } from './portal-resources.mjs';
import { isFormACompletion } from '../src/utils/jotform-completion.ts';
import { portalLinkPresentation } from '../src/utils/portal-presentation.ts';
import {
PortalDownloadError,
fetchProtectedPortalResource,
portalDownloadErrorMessage,
portalRoleForStreamId,
protectedPortalResource,
} from '../src/utils/portal-download.mjs';
import { applyRoleMappings, approvedMapping, summarizeMappings } from './portal-production.mjs';
test('completion envelope requires an exact approved origin, active frame window and string Form A ID', () => {
@@ -48,6 +55,59 @@ test('portal presentation translates only explicit button tokens/brand fills and
assert.equal(portalLinkPresentation([], '', 'tel:'), 'portal-contact-link');
});
test('portal downloads use only the validated role and an in-memory authorization header', async () => {
assert.equal(portalRoleForStreamId('b'), 'staff');
assert.equal(portalRoleForStreamId('a'), 'management');
assert.equal(portalRoleForStreamId('fixture-staff'), undefined);
const origin = 'https://www.azinstitute4autism.com';
const staff = protectedPortalResource('/staff-resources/staff-guide.pdf', origin);
const management = protectedPortalResource('/management-resources/management-guide.pdf', origin);
assert.deepEqual(staff, {
role: 'staff', filename: 'staff-guide.pdf',
url: origin + '/staff-resources/staff-guide.pdf',
});
assert.equal(protectedPortalResource('/staff-resources/nested/file.pdf', 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);
const code = 'synthetic browser memory only';
let captured;
const result = await fetchProtectedPortalResource(staff, { role: 'staff', code }, async (url, options) => {
captured = { url, options };
return new Response('synthetic document', {
headers: { 'content-disposition': 'attachment; filename="approved-name.pdf"' },
});
});
assert.equal(captured.url, staff.url);
assert.equal(captured.url.includes(code), false);
assert.equal(captured.options.method, 'GET');
assert.equal(captured.options.credentials, 'omit');
assert.equal(captured.options.cache, 'no-store');
assert.equal(captured.options.redirect, 'error');
assert.equal(captured.options.referrerPolicy, 'no-referrer');
assert.match(captured.options.headers.Authorization, /^Basic /);
assert.equal(result.filename, 'approved-name.pdf');
assert.equal(await result.blob.text(), 'synthetic document');
for (const [resource, credential] of [
[management, { role: 'staff', code }],
[staff, { role: 'management', code }],
]) {
let called = false;
await assert.rejects(
fetchProtectedPortalResource(resource, credential, async () => { called = true; return new Response(); }),
(error) => error instanceof PortalDownloadError && error.code === 'role',
);
assert.equal(called, false, 'cross-role credentials are never transmitted');
}
await assert.rejects(
fetchProtectedPortalResource(staff, { role: 'staff', code }, async () => new Response('', { status: 401 })),
(error) => error instanceof PortalDownloadError && error.code === 'unauthorized',
);
assert.match(portalDownloadErrorMessage(new PortalDownloadError('unauthorized')), /session could not authorize/);
assert.match(portalDownloadErrorMessage(new PortalDownloadError('role')), /not available from this portal/);
});
test('staff template presentation follows sanitized headings, not original HubSpot classes', async () => {
const renderer = await fs.readFile('src/scripts/employee-portal.ts', 'utf8');
const css = await fs.readFile('src/components/pages/EmployeePortal.astro', 'utf8');