Add isolated artifact owner consent page

This commit is contained in:
Thomas Durieux committed 2026-09-27 14:56:06 +00:00
1 parent 7b60979c96
commit e4334bc9a7
10 files changed
+443 -3

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
require("ts-node/register/transpile-only");
const { expect } = require("chai");
const express = require("express");
const http = require("http");
const fs = require("fs");
const os = require("os");
const path = require("path");
const {
createReviewConsentPage,
isReviewConsentPagePath,
} = require("../src/server/review-consent-page");
describe("isolated review consent page", function () {
let server, directory, manifest;
beforeEach(async function () {
directory = fs.mkdtempSync(path.join(os.tmpdir(), "review-page-"));
manifest = path.join(directory, "manifest.json");
const app = express();
const page = createReviewConsentPage(manifest);
app.use((req, res) =>
isReviewConsentPagePath(req.path)
? page(req, res)
: res.status(418).end(),
);
server = await new Promise((resolve) => {
const s = app.listen(0, "127.0.0.1", () => resolve(s));
});
});
afterEach(async function () {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(directory, { recursive: true, force: true });
});
function get(url, method = "GET") {
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: "127.0.0.1",
port: server.address().port,
path: url,
method,
},
(res) => {
let body = "";
res.on("data", (chunk) => {
body += chunk;
});
res.on("end", () =>
resolve({ status: res.statusCode, headers: res.headers, body }),
);
},
);
req.on("error", reject);
req.end();
});
}
it("serves only first-party assets with no inline scripts or external frames", async function () {
const response = await get("/review-link");
expect(response.status).equal(200);
expect(response.headers["cache-control"]).equal("no-store");
expect(response.headers["referrer-policy"]).equal("no-referrer");
expect(response.headers["x-frame-options"]).equal("DENY");
expect(response.headers["content-security-policy"])
.include("script-src 'self'")
.include("frame-ancestors 'none'");
expect(response.body).include('name="review-consent-page"');
expect(response.body).not.match(/https?:|kofi|ko-fi|<iframe|<script\s*>/i);
expect(response.body.match(/<script /g)).length(2);
expect(response.body)
.include("/script/core.min.js")
.include("/script/vendor.min.js");
});
it("reserves encoded, case and slash aliases and rejects query strings and writes", async function () {
for (const url of [
"/review-link/",
"/REVIEW-LINK",
"/%72eview-link",
"/review-link?token=secret",
"/review-link///",
]) {
const response = await get(url);
expect(response.status, url).equal(404);
expect(response.body).not.include("<html");
expect(response.headers["referrer-policy"]).equal("no-referrer");
}
expect((await get("/review-link", "POST")).status).equal(404);
expect((await get("/review-link-other")).status).equal(418);
expect(isReviewConsentPagePath("/%")).equal(false);
});
it("accepts hashed filenames but rejects manifest URL and HTML injection", async function () {
fs.writeFileSync(
manifest,
JSON.stringify({
"core.min.js": "core.abc123.js",
"vendor.min.js": "//evil.test/x.js",
"all.min.css": 'x.css" onload="alert(1)',
}),
);
const response = await get("/review-link");
expect(response.body)
.include("/script/core.abc123.js")
.include("/script/vendor.min.js")
.include("/css/all.min.css");
expect(response.body).not.include("evil.test").not.include("onload");
fs.writeFileSync(manifest, "null");
expect((await get("/review-link")).status).equal(200);
fs.writeFileSync(manifest, "{");
expect((await get("/review-link")).status).equal(200);
});
it("serves HEAD without a body", async function () {
const response = await get("/review-link", "HEAD");
expect(response.status).equal(200);
expect(response.body).equal("");
});
it("measures 100 isolated page responses", async function () {
const times = [];
const { performance } = require("perf_hooks");
for (let i = 0; i < 100; i++) {
const start = performance.now();
expect((await get('/review-link')).status).equal(200);
times.push(performance.now() - start);
}
times.sort((a, b) => a - b);
const output = require("process").env.TEST_REVIEW_PAGE_PERF_REPORT;
if (output) fs.writeFileSync(output, JSON.stringify({ requests: 100, workload: 'isolated HTML shell, loopback HTTP, no browser or provider', medianMs: times[49], p95Ms: times[94] }, null, 2) + '\n');
});
});
+130
View File
@@ -0,0 +1,130 @@
const { expect } = require("chai");
const { JSDOM, VirtualConsole } = require("jsdom");
const fs = require("fs");
const { URL } = require("url");
const path = require("path");
const { setTimeout: delay } = require("timers/promises");
const bundles = ["core.min.js", "vendor.min.js"].map(name => fs.readFileSync(path.join(__dirname, "../public/script", name), "utf8"));
const token = "3".repeat(64);
const fragment = `clientId=${"1".repeat(32)}&intentId=${"2".repeat(32)}&token=${token}`;
const csrf = "a".repeat(64);
const preview = () => ({ ticket: 'private-ticket', repositoryName: 'Synthetic repository', policy: { version: 1, access: 'restricted-review', retainUntil: '2099-01-01T00:00:00Z' }, expiresAt: new Date(Date.now() + 60000).toISOString() });
async function browser(overrides = {}, hash = fragment) {
const errors = [], requests = [];
const vc = new VirtualConsole();
vc.on('jsdomError', error => errors.push(error.message));
vc.on('error', error => errors.push(String(error)));
const dom = new JSDOM('<!doctype html><html><head><meta name="review-consent-page" content="1"></head><body><div id="app"></div></body></html>', { url: 'https://anonymous.example.test/review-link#' + hash, runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc });
const w = dom.window;
w.matchMedia = () => ({ matches: false });
w.HTMLCanvasElement.prototype.getContext = () => null;
w.fetch = async (url, options = {}) => {
const pathname = new URL(url).pathname;
const request = { pathname, ...options, payload: options.body ? JSON.parse(options.body) : null };
requests.push(request);
let data;
if (overrides[pathname]) data = await overrides[pathname](request);
else if (pathname === '/api/user') data = { username: 'owner' };
else if (pathname === '/api/review-consent/csrf') data = { csrf };
else if (pathname === '/api/review-consent/preview') data = preview();
else if (pathname === '/api/review-consent/confirm') data = { requestId: request.payload.requestId, confirmedAt: new Date().toISOString() };
else data = [];
const status = data?.__status || 200;
return { ok: status < 400, status, headers: { get: () => 'application/json' }, text: async () => JSON.stringify(data?.__status ? data.body : data) };
};
bundles.forEach(bundle => w.eval(bundle));
const app = w.anonymousApp;
await app.router.isReady(); await delay(30);
const document = w.document;
const click = async selector => { document.querySelector(selector).click(); await delay(30); };
return { w, app, document, errors, requests, click,
async preview() { const input = document.querySelector('#review-repository'); input.value = 'synthetic-repo'; input.dispatchEvent(new w.Event('input', { bubbles: true })); await click('button[type=submit]'); },
async accept() { for (const input of document.querySelectorAll('input[type=checkbox]')) { input.click(); } await delay(5); },
close() { app.app.unmount(); w.close(); },
};
}
describe('review consent browser page', function () {
this.timeout(10000);
let b;
afterEach(() => b?.close());
it('scrubs the fragment before requests and keeps secrets out of rendered state and storage', async function () {
b = await browser();
expect(b.w.location.href).equal('https://anonymous.example.test/review-link');
expect(JSON.stringify(b.w.history.state)).not.include(token);
await b.preview();
expect(b.document.body.textContent).include('Synthetic repository');
expect(b.document.documentElement.outerHTML).not.include(token).not.include('private-ticket');
expect(Object.keys(b.w.localStorage)).deep.equal(['darkMode']);
expect(JSON.stringify(b.w.localStorage)).not.include(token).not.include('private-ticket');
expect(b.w.sessionStorage.length).equal(0);
expect(b.document.querySelector('.app-header')).equal(null);
expect(b.requests.find(r => r.pathname.endsWith('/preview')).payload.intent.token).equal(token);
expect(b.errors).deep.equal([]);
});
it('requires both approvals and preserves the confirmation identity after response loss', async function () {
let attempts = 0;
b = await browser({ '/api/review-consent/confirm': request => { if (++attempts === 1) throw new Error('lost response'); return { requestId: request.payload.requestId, confirmedAt: new Date().toISOString() }; } });
await b.preview();
expect(b.document.querySelector('button[type=button]').disabled).equal(true);
await b.accept(); await b.click('button[type=button]');
expect(b.document.body.textContent).include('Retry confirmation');
expect(b.document.querySelector('#review-repository').disabled).equal(true);
await b.click('button[type=button]');
const writes = b.requests.filter(r => r.pathname.endsWith('/confirm'));
expect(writes).length(2); expect(writes[0].payload).deep.equal(writes[1].payload);
expect(b.document.body.textContent).include('Consent recorded. Artifact linking is not complete.');
});
it('does not send a preview after departure during the CSRF request', async function () {
let resolve;
b = await browser({ '/api/review-consent/csrf': () => new Promise(yes => { resolve = yes; }) });
await b.preview(); b.app.app.unmount(); resolve({ csrf }); await delay(30);
expect(b.requests.filter(r => r.pathname.endsWith('/preview'))).length(0);
expect(b.requests.find(r => r.pathname.endsWith('/csrf')).signal.aborted).equal(true);
});
it('discards a late preview after account change', async function () {
let resolve;
b = await browser({ '/api/review-consent/preview': () => new Promise(yes => { resolve = yes; }) });
await b.preview(); b.app.state.user = { username: 'someone-else' }; await delay(5); resolve(preview()); await delay(30);
expect(b.document.body.textContent).include('Your account changed');
expect(b.document.body.textContent).not.include('Synthetic repository');
});
it('rejects duplicate or missing fragment fields without making consent requests', async function () {
b = await browser({}, fragment + '&token=' + token); await b.preview();
expect(b.document.body.textContent).include('missing or invalid');
expect(b.requests.some(r => r.pathname.startsWith('/api/review-consent'))).equal(false);
expect(b.w.location.hash).equal('');
});
it('shows unavailable endpoints and keeps the intent request ID on preview retry', async function () {
let attempts = 0;
b = await browser({ '/api/review-consent/preview': () => ++attempts === 1 ? { __status: 404, body: {} } : preview() });
await b.preview(); expect(b.document.body.textContent).include('not available'); await b.preview();
const requests = b.requests.filter(r => r.pathname.endsWith('/preview'));
expect(requests[0].payload.intent.requestId).equal(requests[1].payload.intent.requestId);
});
it('does not approve an expired preview', async function () {
b = await browser({ '/api/review-consent/preview': () => ({ ...preview(), expiresAt: new Date(Date.now() + 1000).toISOString() }) });
await b.preview(); await b.accept(); await delay(1050); await b.click('button[type=button]');
expect(b.requests.some(r => r.pathname.endsWith('/confirm'))).equal(false);
expect(b.document.body.textContent).include('preview expired');
});
it('requires sign-in without retaining the handoff in browser storage', async function () {
b = await browser({ '/api/user': () => null });
expect(b.document.querySelector('#review-repository')).equal(null);
expect(b.document.body.textContent).include('Sign in first');
expect(b.w.location.hash).equal('');
expect(b.w.sessionStorage.length).equal(0);
expect(b.requests.some(r => r.pathname.startsWith('/api/review-consent'))).equal(false);
});
it('rejects malformed policy previews and mismatched confirmation receipts', async function () {
let attempts = 0;
b = await browser({
'/api/review-consent/preview': () => ++attempts === 1 ? { ...preview(), policy: { ...preview().policy, access: 'unsupported' } } : preview(),
'/api/review-consent/confirm': () => ({ requestId: 'wrong', confirmedAt: new Date().toISOString() }),
});
await b.preview(); expect(b.document.querySelector('input[type=checkbox]')).equal(null);
await b.preview(); await b.accept(); await b.click('button[type=button]');
expect(b.document.body.textContent).include('confirmation was not verified');
expect(b.document.body.textContent).not.include('Consent recorded.');
});
});