From dfa12dd862188dc2ae26c6cdb8c24f2503d62364 Mon Sep 17 00:00:00 2001
From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com>
Date: Thu, 5 Nov 2026 01:59:51 +0000
Subject: [PATCH 1/2] test: prepare bounded offline Rust browser and telemetry
checks
---
package.json | 2 +-
security/browser-headers/README.md | 5 +
security/browser-headers/runtime/.gitignore | 1 +
.../browser-headers/runtime/package-lock.json | 42 ++++++
security/browser-headers/runtime/package.json | 8 +
.../browser-headers/rust-browser-inputs.cjs | 129 ++++++++++++++++
.../rust-browser-source.test.cjs | 98 +++++++++++++
.../browser-headers/rust-browser.test.cjs | 138 +++++++++++++-----
.../rust-local-browser-packet.json | 36 +++++
.../rust-local-browser-plan.md | 137 +++++++++++++++++
10 files changed, 559 insertions(+), 37 deletions(-)
create mode 100644 security/browser-headers/runtime/.gitignore
create mode 100644 security/browser-headers/runtime/package-lock.json
create mode 100644 security/browser-headers/runtime/package.json
create mode 100644 security/browser-headers/rust-browser-inputs.cjs
create mode 100644 security/browser-headers/rust-browser-source.test.cjs
create mode 100644 security/browser-headers/rust-local-browser-packet.json
create mode 100644 security/browser-headers/rust-local-browser-plan.md
diff --git a/package.json b/package.json
index a2f6d119f97..d8f8511e8b2 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
- "test:browser-security-headers": "node --test scripts/browser-security-headers.test.js"
+ "test:browser-security-headers": "node --test --test-concurrency=2 scripts/browser-security-headers.test.js security/browser-headers/rust-browser-source.test.cjs"
},
"dependencies": {
"@docusaurus/core": "3.10.2",
diff --git a/security/browser-headers/README.md b/security/browser-headers/README.md
index cf15119bb0b..46c27f9ee34 100644
--- a/security/browser-headers/README.md
+++ b/security/browser-headers/README.md
@@ -111,6 +111,11 @@ a browser compatibility check. No current-artifact browser run or provider stage
is authorized by this source proposal. Follow `rust-stage-proposal.md` for the
exact input bindings, ownership, proposed checks and rollback decision.
+`rust-local-browser-plan.md` and `rust-local-browser-packet.json` define the
+separately reviewable, unrun local browser/telemetry packet. Normal CI exercises
+only its inert source controls. The optional browser entrypoint requires its
+own exact run admission; it makes no provider or production request.
+
## Rust-only staged acceptance and rollback
1. Bind the accepted package, source/tooling revisions, protected artifact ZIP
diff --git a/security/browser-headers/runtime/.gitignore b/security/browser-headers/runtime/.gitignore
new file mode 100644
index 00000000000..c2658d7d1b3
--- /dev/null
+++ b/security/browser-headers/runtime/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/security/browser-headers/runtime/package-lock.json b/security/browser-headers/runtime/package-lock.json
new file mode 100644
index 00000000000..e44f2c0076c
--- /dev/null
+++ b/security/browser-headers/runtime/package-lock.json
@@ -0,0 +1,42 @@
+{
+ "name": "durable-workflow-rust-browser-check",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "durable-workflow-rust-browser-check",
+ "version": "0.0.0",
+ "dependencies": {
+ "playwright": "1.64.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.64.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz",
+ "integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.64.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.64.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz",
+ "integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ }
+ }
+}
diff --git a/security/browser-headers/runtime/package.json b/security/browser-headers/runtime/package.json
new file mode 100644
index 00000000000..96d8d97f4f2
--- /dev/null
+++ b/security/browser-headers/runtime/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "durable-workflow-rust-browser-check",
+ "private": true,
+ "version": "0.0.0",
+ "dependencies": {
+ "playwright": "1.64.0"
+ }
+}
diff --git a/security/browser-headers/rust-browser-inputs.cjs b/security/browser-headers/rust-browser-inputs.cjs
new file mode 100644
index 00000000000..9aebc75b840
--- /dev/null
+++ b/security/browser-headers/rust-browser-inputs.cjs
@@ -0,0 +1,129 @@
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const ORIGIN = 'https://rust.durable-workflow.com';
+const BEACON = 'https://static.cloudflareinsights.com/beacon.min.js';
+const RUM = 'https://cloudflareinsights.com/cdn-cgi/rum';
+const PROMOTION = 'https://cloud.durable-workflow.com/early-access/promotion-events';
+const IMAGE_ORIGINS = new Set(['https://github.com', 'https://img.shields.io', 'https://sonarcloud.io']);
+const MIME = {'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf'};
+const digest = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
+
+function verifyPacketFiles(directory) {
+ const packet = JSON.parse(fs.readFileSync(path.join(directory, 'rust-local-browser-packet.json')));
+ assert.equal(packet.run_status, 'not-run');
+ for (const [name, hash] of Object.entries(packet.files)) {
+ assert.ok(!path.isAbsolute(name) && !name.split('/').includes('..'), 'Unsafe packet path');
+ assert.equal(digest(fs.readFileSync(path.join(directory, name))), hash, 'Packet member changed');
+ }
+ return packet;
+}
+
+function loadInputs(root, packet, inventory) {
+ assert.equal(packet.rule.enabled, false);
+ assert.equal(packet.rollout_approved, false);
+ assert.equal(packet.compatibility_accepted, false);
+ assert.equal(inventory.origin, ORIGIN);
+ assert.equal(packet.inventory_sha256, digest(JSON.stringify(inventory, null, 2) + '\n'));
+ assert.deepEqual(packet.input_artifact, inventory.input_artifact);
+ assert.equal(packet.source_preparation, null);
+ assert.equal(inventory.source_preparation, null);
+ assert.ok(fs.lstatSync(root).isDirectory(), 'Artifact root must be a regular directory');
+ const files = new Map();
+ let bytes = 0;
+ function visit(directory) {
+ for (const entry of fs.readdirSync(directory, {withFileTypes: true})) {
+ const absolute = path.join(directory, entry.name);
+ assert.ok(!entry.isSymbolicLink(), 'Artifact symlinks are refused');
+ if (entry.isDirectory()) visit(absolute);
+ else {
+ assert.ok(entry.isFile(), 'Artifact special files are refused');
+ const relative = path.relative(root, absolute).split(path.sep).join('/');
+ const size = fs.statSync(absolute).size;
+ bytes += size;
+ assert.ok(bytes <= 64 * 1024 * 1024 && files.size < 1000, 'Artifact exceeds the local input bound');
+ files.set(relative, fs.readFileSync(absolute));
+ }
+ }
+ }
+ visit(root);
+ assert.equal(files.size, inventory.files.length, 'Artifact file count changed');
+ const seen = new Set();
+ for (const record of inventory.files) {
+ assert.ok(!seen.has(record.path), 'Duplicate inventory path');
+ seen.add(record.path);
+ const content = files.get(record.path);
+ assert.ok(content, 'Inventoried artifact file is missing');
+ assert.equal(content.length, record.bytes, 'Artifact file size changed');
+ assert.equal(digest(content), record.sha256, 'Artifact file digest changed');
+ }
+ assert.deepEqual(JSON.parse(files.get('__build.json')), inventory.build_identity);
+ return {files, packet, inventory, headers: Object.fromEntries(Object.entries(packet.rule.action_parameters.headers).map(([name, record]) => [name, record.value]))};
+}
+
+function promotionEvent(request) {
+ assert.equal(request.method, 'POST', 'Promotion request method changed');
+ const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));
+ assert.ok(!Object.hasOwn(headers, 'authorization'), 'Local telemetry cannot carry authorization');
+ assert.ok(!Object.hasOwn(headers, 'cookie'), 'Local telemetry cannot carry cookies');
+ assert.match(headers['content-type'] || '', /^text\/plain(?:;|$)/);
+ assert.ok(typeof request.body === 'string' && request.body.length < 256, 'Promotion body exceeds its bound');
+ const event = JSON.parse(request.body);
+ assert.deepEqual(Object.keys(event).sort(), ['event', 'source']);
+ assert.equal(event.source, 'sdk-rust-reference');
+ assert.ok(['impression', 'click'].includes(event.event), 'Unknown promotion event');
+ return event.event;
+}
+
+function localResponse(input, request, origin = ORIGIN) {
+ assert.ok([ORIGIN, 'https://example.invalid'].includes(origin), 'Unexpected local origin');
+ const requestHeaders = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));
+ assert.ok(!Object.hasOwn(requestHeaders, 'authorization'), 'Local requests cannot carry authorization');
+ assert.ok(!Object.hasOwn(requestHeaders, 'cookie'), 'Local requests cannot carry cookies');
+ const url = new URL(request.url);
+ if (url.origin === origin) {
+ assert.equal(request.method, 'GET', 'Artifact requests must be GET');
+ const relative = decodeURIComponent(url.pathname).replace(/^\//, '');
+ if (relative === '__frame_probe') return {kind: 'frame-probe', status: 200, headers: {'content-type': 'text/html'}, body: '
Framing control'};
+ if (relative === '__telemetry_fixture') return {kind: 'promotion-dom-fixture', status: 200, headers: {...input.headers, 'content-type': 'text/html'}, body: 'Synthetic promotion control'};
+ const file = [relative, relative.replace(/\/$/, '') + '/index.html', relative ? null : 'index.html'].find((name) => name && input.files.has(name));
+ if (!file) return {kind: 'missing', status: 404, body: ''};
+ return {kind: 'artifact', status: 200, headers: {...input.headers, 'content-type': MIME[path.extname(file)] || 'application/octet-stream', 'cache-control': 'no-store'}, body: input.files.get(file)};
+ }
+ const cors = {'access-control-allow-origin': origin};
+ if (request.url === BEACON && request.method === 'GET') {
+ // This finite fixture exercises CSP loader/connect paths, never vendor code or delivery.
+ return {kind: 'beacon-fixture', status: 200, headers: {...cors, 'content-type': 'text/javascript'}, body: `globalThis.__localBeaconFixture = true; fetch('${RUM}', {method: 'POST', credentials: 'omit', headers: {'Content-Type': 'text/plain'}, body: 'local-csp-probe'});`};
+ }
+ if (request.url === RUM && request.method === 'POST' && request.body === 'local-csp-probe') {
+ return {kind: 'rum-fixture', status: 204, headers: cors, body: ''};
+ }
+ if (request.url === PROMOTION) return {kind: 'promotion-' + promotionEvent(request), status: 204, headers: cors, body: ''};
+ if (IMAGE_ORIGINS.has(url.origin) && request.method === 'GET' && request.resourceType === 'image') {
+ return {kind: 'image-fixture', status: 200, headers: {'content-type': 'image/svg+xml'}, body: ''};
+ }
+ throw new Error('Request is outside the finite local response set');
+}
+
+async function installLocalRoutes(context, input, origin = ORIGIN) {
+ const audit = {requests: 0, kinds: {}, refused: []};
+ await context.routeWebSocket('**/*', (socket) => { audit.refused.push('websocket'); socket.close(); });
+ await context.route('**/*', async (route) => {
+ try {
+ audit.requests += 1;
+ assert.ok(audit.requests <= 4000, 'Local request budget exceeded');
+ const request = route.request();
+ const response = localResponse(input, {url: request.url(), method: request.method(), headers: request.headers(), body: request.postData(), resourceType: request.resourceType()}, origin);
+ audit.kinds[response.kind] = (audit.kinds[response.kind] || 0) + 1;
+ await route.fulfill({status: response.status, headers: response.headers, body: response.body});
+ } catch {
+ audit.refused.push('http');
+ await route.abort();
+ }
+ });
+ return audit;
+}
+
+module.exports = {ORIGIN, BEACON, RUM, PROMOTION, digest, verifyPacketFiles, loadInputs, localResponse, installLocalRoutes};
diff --git a/security/browser-headers/rust-browser-source.test.cjs b/security/browser-headers/rust-browser-source.test.cjs
new file mode 100644
index 00000000000..8bc8133b815
--- /dev/null
+++ b/security/browser-headers/rust-browser-source.test.cjs
@@ -0,0 +1,98 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const test = require('node:test');
+const {ORIGIN, BEACON, RUM, PROMOTION, digest, loadInputs, localResponse, installLocalRoutes} = require('./rust-browser-inputs.cjs');
+
+function fixture(t) {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-browser-source-'));
+ t.after(() => fs.rmSync(root, {recursive: true, force: true}));
+ const build = {version: 'fixture'};
+ const files = {'__build.json': JSON.stringify(build), 'index.html': 'Fixture
'};
+ for (const [name, value] of Object.entries(files)) fs.writeFileSync(path.join(root, name), value);
+ const inventory = {origin: ORIGIN, input_artifact: {artifact_id: 'fixture'}, source_preparation: null, build_identity: build, files: Object.entries(files).map(([name, content]) => ({path: name, bytes: Buffer.byteLength(content), sha256: digest(content)}))};
+ const packet = {rule: {enabled: false, action_parameters: {headers: {'content-security-policy': {value: "default-src 'none'"}}}}, rollout_approved: false, compatibility_accepted: false, source_preparation: null, input_artifact: inventory.input_artifact, inventory_sha256: digest(JSON.stringify(inventory, null, 2) + '\n')};
+ return {root, inventory, packet};
+}
+
+function request(url, options = {}) {
+ return {url, method: 'GET', headers: {}, body: null, resourceType: 'script', ...options};
+}
+
+test('verifies every input file and refuses drift before a browser is needed', (t) => {
+ const value = fixture(t);
+ assert.equal(loadInputs(value.root, value.packet, value.inventory).files.size, 2);
+ fs.writeFileSync(path.join(value.root, 'index.html'), 'Changed
');
+ assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /Artifact file/);
+});
+
+test('refuses extra files, symlinks, duplicate inventory and changed approval flags', (t) => {
+ const value = fixture(t);
+ fs.writeFileSync(path.join(value.root, 'extra'), 'extra');
+ assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /file count/);
+ fs.unlinkSync(path.join(value.root, 'extra'));
+ fs.symlinkSync(path.join(value.root, 'index.html'), path.join(value.root, 'link'));
+ assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /symlinks/);
+ fs.unlinkSync(path.join(value.root, 'link'));
+ for (const mutate of [(p) => { p.rule.enabled = true; }, (p) => { p.rollout_approved = true; }, (p) => { p.compatibility_accepted = true; }]) {
+ const packet = structuredClone(value.packet); mutate(packet);
+ assert.throws(() => loadInputs(value.root, packet, value.inventory));
+ }
+ value.inventory.files[1] = value.inventory.files[0];
+ value.packet.inventory_sha256 = digest(JSON.stringify(value.inventory, null, 2) + '\n');
+ assert.throws(() => loadInputs(value.root, value.packet, value.inventory), /Duplicate inventory/);
+});
+
+test('serves only verified buffers and finite external fixtures', (t) => {
+ const value = fixture(t);
+ const input = loadInputs(value.root, value.packet, value.inventory);
+ assert.equal(localResponse(input, request(ORIGIN + '/')).body.toString(), 'Fixture
');
+ assert.equal(localResponse(input, request(ORIGIN + '/%2e%2e/private')).status, 404);
+ assert.equal(localResponse(input, request(BEACON)).kind, 'beacon-fixture');
+ assert.equal(localResponse(input, request(RUM, {method: 'POST', body: 'local-csp-probe'})).kind, 'rum-fixture');
+ for (const url of ['https://unreviewed.invalid/x', 'file:///private', BEACON + '?extra=1', RUM + '/extra']) assert.throws(() => localResponse(input, request(url)));
+ assert.throws(() => localResponse(input, request(BEACON, {headers: {Cookie: 'fixture'}})), /cookies/);
+ assert.throws(() => localResponse(input, request(BEACON, {headers: {Authorization: 'fixture'}})), /authorization/);
+});
+
+test('promotion fixtures require exact method, event shape and credential absence', (t) => {
+ const value = fixture(t);
+ const input = loadInputs(value.root, value.packet, value.inventory);
+ const valid = request(PROMOTION, {method: 'POST', headers: {'content-type': 'text/plain'}, body: JSON.stringify({source: 'sdk-rust-reference', event: 'impression'})});
+ assert.equal(localResponse(input, valid).kind, 'promotion-impression');
+ for (const change of [
+ {method: 'GET'}, {body: '{invalid'}, {body: JSON.stringify({source: 'other', event: 'click'})},
+ {body: JSON.stringify({source: 'sdk-rust-reference', event: 'other'})},
+ {body: JSON.stringify({source: 'sdk-rust-reference', event: 'click', extra: true})},
+ ]) assert.throws(() => localResponse(input, {...valid, ...change}));
+});
+
+test('fake routing fulfills or aborts without a network continuation', async (t) => {
+ const value = fixture(t);
+ const input = loadInputs(value.root, value.packet, value.inventory);
+ let httpHandler; let websocketHandler;
+ const context = {route: async (_pattern, handler) => { httpHandler = handler; }, routeWebSocket: async (_pattern, handler) => { websocketHandler = handler; }};
+ const audit = await installLocalRoutes(context, input);
+ async function dispatch(url) {
+ const req = request(url);
+ const route = {request: () => ({url: () => req.url, method: () => req.method, headers: () => req.headers, postData: () => req.body, resourceType: () => req.resourceType}), fulfill: async () => 'fulfilled', abort: async () => 'aborted'};
+ await httpHandler(route);
+ }
+ await dispatch(ORIGIN + '/');
+ await dispatch('https://unreviewed.invalid/x');
+ let closed = false;
+ websocketHandler({close: () => { closed = true; }});
+ assert.equal(closed, true);
+ assert.equal(audit.kinds.artifact, 1);
+ assert.deepEqual(audit.refused, ['http', 'websocket']);
+});
+
+test('review packet identifies the exact executable and data closure', () => {
+ const packet = JSON.parse(fs.readFileSync(path.join(__dirname, 'rust-local-browser-packet.json')));
+ assert.equal(packet.run_status, 'not-run');
+ for (const [name, hash] of Object.entries(packet.files)) assert.equal(digest(fs.readFileSync(path.join(__dirname, name))), hash, name);
+ const candidate = JSON.parse(fs.readFileSync(path.join(__dirname, 'rust-rule.candidate.json')));
+ assert.deepEqual(packet.input_artifact, candidate.input_artifact);
+ assert.equal(packet.container.network, 'none');
+});
diff --git a/security/browser-headers/rust-browser.test.cjs b/security/browser-headers/rust-browser.test.cjs
index 7f805eaec09..32ee55b0276 100644
--- a/security/browser-headers/rust-browser.test.cjs
+++ b/security/browser-headers/rust-browser.test.cjs
@@ -1,49 +1,26 @@
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
-const http = require('node:http');
const path = require('node:path');
const test = require('node:test');
+const {ORIGIN, PROMOTION, verifyPacketFiles, loadInputs, installLocalRoutes} = require('./rust-browser-inputs.cjs');
+const reviewedPacket = verifyPacketFiles(__dirname);
+assert.equal(require('playwright/package.json').version, reviewedPacket.playwright_version);
const {chromium} = require('playwright');
test('Rust artifact CSP and navigation compatibility', {timeout: 150000}, async () => {
const root = path.resolve(process.argv[2]);
const packet = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
const inventory = JSON.parse(fs.readFileSync(process.argv[4], 'utf8'));
- assert.equal(packet.rule.enabled, false);
- assert.equal(packet.rollout_approved, false);
- assert.equal(crypto.createHash('sha256').update(JSON.stringify(inventory, null, 2) + '\n').digest('hex'), packet.inventory_sha256);
- const headers = Object.fromEntries(Object.entries(packet.rule.action_parameters.headers).map(([name, record]) => [name, record.value]));
- const mime = {'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf'};
- const server = http.createServer((request, response) => {
- const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
- if (pathname === '/__frame_probe') {
- response.writeHead(200, {'content-type': 'text/html'});
- response.end('Framing control');
- return;
- }
- let file = path.resolve(root, '.' + pathname);
- if (file !== root && !file.startsWith(root + path.sep)) { response.writeHead(403); response.end(); return; }
- if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, 'index.html');
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end(); return; }
- response.writeHead(200, {...headers, 'content-type': mime[path.extname(file)] || 'application/octet-stream', 'cache-control': 'no-store'});
- fs.createReadStream(file).pipe(response);
- });
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
- const origin = `http://127.0.0.1:${server.address().port}`;
+ const input = loadInputs(root, packet, inventory);
+ const headers = input.headers;
+ const origin = ORIGIN;
const browser = await chromium.launch({headless: true});
try {
- const context = await browser.newContext();
- const unexpected = [];
- const allowed = new Set(['https://github.com', 'https://img.shields.io', 'https://sonarcloud.io', 'https://static.cloudflareinsights.com', 'https://cloudflareinsights.com', 'https://cloud.durable-workflow.com']);
- await context.route('**/*', async (route) => {
- const url = new URL(route.request().url());
- if (url.origin === origin) return route.continue();
- if (!allowed.has(url.origin)) { unexpected.push(url.origin + url.pathname); return route.abort(); }
- // Public badges/telemetry are intercepted: this local check sends no production request.
- if (route.request().resourceType() === 'image') return route.fulfill({contentType: 'image/svg+xml', body: ''});
- return route.fulfill({status: 200, contentType: 'text/javascript', body: ''});
- });
+ assert.equal(browser.version(), reviewedPacket.expected_chromium_version);
+ const context = await browser.newContext({serviceWorkers: 'block'});
+ context.setDefaultTimeout(10000);
+ const audit = await installLocalRoutes(context, input);
await context.addInitScript(() => {
window.__policyViolations = [];
document.addEventListener('securitypolicyviolation', (event) => window.__policyViolations.push({directive: event.effectiveDirective, blocked: event.blockedURI}));
@@ -104,7 +81,8 @@ test('Rust artifact CSP and navigation compatibility', {timeout: 150000}, async
});
await page.waitForTimeout(500);
assert.ok(frameMessages.length > 0, 'response frame-ancestors policy must block the framing control');
- assert.deepEqual(unexpected, []);
+ assert.deepEqual(audit.refused, []);
+ assert.equal(audit.kinds['beacon-fixture'], undefined);
console.log(JSON.stringify({
outcome: 'pass', scope: 'Local artifact CSP compatibility only; no TLS/HSTS or public-rollout acceptance',
browser: browser.version(), pages_checked: pages.size, inline_hash_representatives: representatives.size,
@@ -113,10 +91,98 @@ test('Rust artifact CSP and navigation compatibility', {timeout: 150000}, async
inventory_sha256: packet.inventory_sha256,
csp_sha256: crypto.createHash('sha256').update(headers['content-security-policy']).digest('hex'),
checks: ['all inventoried inline-script hash representatives', 'search', 'history back', 'direct-entry fallback', 'compact navigation', 'unapproved inline script refused', 'response framing refused'],
- external_requests: 'intercepted locally; none sent to production',
+ external_requests: 'fulfilled or refused locally; container network must be disabled',
+ request_audit: audit,
+ vendor_and_production_telemetry: 'not tested',
}));
} finally {
await browser.close();
- await new Promise((resolve) => server.close(resolve));
+ }
+});
+
+async function instrumentTelemetry(context, nonWebdriver) {
+ // Harness-only instrumentation. Native fetch still applies CSP and CORS.
+ await context.addInitScript(({nonWebdriver, promotionUrl}) => {
+ if (nonWebdriver) Object.defineProperty(Navigator.prototype, 'webdriver', {get: () => false, configurable: true});
+ window.__policyViolations = [];
+ window.__promotionFetchOptions = [];
+ document.addEventListener('securitypolicyviolation', (event) => window.__policyViolations.push(event.effectiveDirective));
+ const nativeFetch = window.fetch;
+ window.fetch = function (resource, options) {
+ if (String(resource) === promotionUrl) {
+ window.__promotionFetchOptions.push({method: options.method, mode: options.mode, credentials: options.credentials, referrerPolicy: options.referrerPolicy, body: options.body});
+ }
+ return nativeFetch.call(this, resource, options);
+ };
+ }, {nonWebdriver, promotionUrl: PROMOTION});
+}
+
+async function checkPromotionFixture(page, audit) {
+ await page.locator('[data-promotion-source="sdk-rust-reference"]').scrollIntoViewIfNeeded();
+ await page.waitForFunction(() => window.__promotionFetchOptions.length === 1 && window.__localBeaconFixture === true);
+ const action = page.locator('[data-promotion-action="early-access"]');
+ // Exercise the real click listener without navigating to the production application.
+ await action.evaluate((element) => element.addEventListener('click', (event) => event.preventDefault()));
+ await action.click();
+ await page.waitForFunction(() => window.__promotionFetchOptions.length === 2);
+ await page.waitForLoadState('networkidle');
+ const calls = await page.evaluate(() => window.__promotionFetchOptions);
+ assert.deepEqual(calls.map((call) => JSON.parse(call.body).event), ['impression', 'click']);
+ for (const call of calls) {
+ assert.equal(call.method, 'POST');
+ assert.equal(call.mode, 'cors');
+ assert.equal(call.credentials, 'omit');
+ assert.equal(call.referrerPolicy, 'no-referrer');
+ }
+ for (const kind of ['promotion-impression', 'promotion-click']) assert.equal(audit.kinds[kind], 1, kind);
+}
+
+async function checkTelemetryControl(browser, input, control) {
+ const context = await browser.newContext({serviceWorkers: 'block', viewport: {width: 1280, height: 900}});
+ context.setDefaultTimeout(10000);
+ const audit = await installLocalRoutes(context, input, control.origin);
+ try {
+ await instrumentTelemetry(context, control.nonWebdriver);
+ const page = await context.newPage();
+ const response = await page.goto(control.origin + control.route, {waitUntil: 'networkidle'});
+ assert.equal(response.status(), 200);
+ assert.equal(await page.evaluate(() => navigator.webdriver), !control.nonWebdriver);
+ if (control.name === 'synthetic-promotion-dom') await checkPromotionFixture(page, audit);
+ else {
+ assert.deepEqual(await page.evaluate(() => window.__promotionFetchOptions), []);
+ for (const kind of ['promotion-impression', 'promotion-click']) assert.equal(audit.kinds[kind], undefined, kind);
+ }
+ for (const kind of ['beacon-fixture', 'rum-fixture']) assert.equal(audit.kinds[kind], control.beaconExpected ? 1 : undefined, kind);
+ assert.deepEqual(await page.evaluate(() => window.__policyViolations), []);
+ if (control.name === 'current-artifact') {
+ assert.equal(await page.locator('[data-promotion-source]').count(), 0);
+ await page.evaluate(async () => { try { await fetch('https://unreviewed.invalid/blocked'); } catch { /* Expected CSP refusal. */ } });
+ await page.waitForFunction(() => window.__policyViolations.includes('connect-src'));
+ }
+ assert.deepEqual(audit.refused, []);
+ return {control: control.name, audit};
+ } finally {
+ await context.close();
+ }
+}
+
+test('Rust analytics branches under local CSP with finite telemetry fixtures', {timeout: 90000}, async () => {
+ const root = path.resolve(process.argv[2]);
+ const packet = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
+ const inventory = JSON.parse(fs.readFileSync(process.argv[4], 'utf8'));
+ const input = loadInputs(root, packet, inventory);
+ const browser = await chromium.launch({headless: true});
+ const results = [];
+ try {
+ assert.equal(browser.version(), reviewedPacket.expected_chromium_version);
+ for (const control of [
+ {name: 'current-artifact', origin: ORIGIN, route: '/durable_workflow/index.html', nonWebdriver: true, beaconExpected: true},
+ {name: 'synthetic-promotion-dom', origin: ORIGIN, route: '/__telemetry_fixture', nonWebdriver: true, beaconExpected: true},
+ {name: 'webdriver-refusal', origin: ORIGIN, route: '/__telemetry_fixture', nonWebdriver: false, beaconExpected: false},
+ {name: 'other-host-refusal', origin: 'https://example.invalid', route: '/__telemetry_fixture', nonWebdriver: true, beaconExpected: false},
+ ]) results.push(await checkTelemetryControl(browser, input, control));
+ console.log(JSON.stringify({outcome: 'pass', scope: 'Local artifact loader and synthetic promotion/CSP branches only; vendor beacon and production delivery not tested', input_artifact: packet.input_artifact, inventory_sha256: packet.inventory_sha256, controls: results, artifact_bytes_modified: false, network_required: 'none', instrumentation: ['non-webdriver getter in controlled contexts', 'promotion fetch-option observation delegating to native fetch', 'synthetic promotion DOM with real unchanged analytics script', 'prevent default external click navigation', 'synthetic beacon and rum responses']}));
+ } finally {
+ await browser.close();
}
});
diff --git a/security/browser-headers/rust-local-browser-packet.json b/security/browser-headers/rust-local-browser-packet.json
new file mode 100644
index 00000000000..f9557dd2678
--- /dev/null
+++ b/security/browser-headers/rust-local-browser-packet.json
@@ -0,0 +1,36 @@
+{
+ "schema": "durable-workflow.local-browser-review-packet/v1",
+ "run_status": "not-run",
+ "input_artifact": {
+ "repository": "durable-workflow/sdk-rust",
+ "run_id": 41939116087,
+ "artifact_id": 13375154247,
+ "tooling_revision": "1ea3bfc3965423534b5bbbed700371f126835fc3",
+ "zip_bytes": 4071555,
+ "zip_sha256": "ff902191bdb90f7f1d047ecc5d27e9b8c5215a0239e4f11fcbf574f7ddab83d1"
+ },
+ "playwright_version": "1.64.0",
+ "expected_chromium_version": "156.0.8078.4",
+ "container": {
+ "image": "mcr.microsoft.com/playwright@sha256:bc72a8df40831ded821e3cbc79c318c21b44d6e51dff9fd89ace7a8249c5b462",
+ "config_sha256": "847f71bbb3f17fc1032d50d75bf6fb3fdcf188b16dff81f24667fe9ae9409e01",
+ "platform": "linux/amd64",
+ "cpus": 2,
+ "memory_bytes": 2147483648,
+ "pids": 256,
+ "network": "none",
+ "deadline_seconds": 300,
+ "kill_grace_seconds": 10,
+ "max_active_browsers": 1
+ },
+ "files": {
+ "rust-browser.test.cjs": "9a9854690a585d5589c17a99b6a92e83b7797289abe3aa3c776268d8612c07ae",
+ "rust-browser-inputs.cjs": "d3ed02b8e6d392b472e3e893a8464cb1ab4255be49abaf5ac262da8b71aee422",
+ "rust-browser-source.test.cjs": "42c86ac5eac34b71388ac4f0943ca55000fafa31243304a7b2f317a7f55be55c",
+ "rust-policy.json": "a357bcc5ea8ad96db5e601e4e1c1cb3c8c7eb3441caa9b331d0c2a906ffe84d8",
+ "rust-inventory.json": "8c4bc2eb1ef3ffeeb9788a9d0b44d7b718a2a56caff55b3d4531716a25b270d7",
+ "rust-rule.candidate.json": "bce0c66db4454bc16941a8f5dadb04706315e673d5e2c33f677e7728bafd0135",
+ "runtime/package.json": "a2cd697a8ed17ab196e4eac1bcb15ceec6da243a600a8f2584c8aabdadfc3c9d",
+ "runtime/package-lock.json": "959580e91aae43c61d0bbfcf11c21eb52ecafcfadb088f166b322fd3988a47ae"
+ }
+}
diff --git a/security/browser-headers/rust-local-browser-plan.md b/security/browser-headers/rust-local-browser-plan.md
new file mode 100644
index 00000000000..73368385a56
--- /dev/null
+++ b/security/browser-headers/rust-local-browser-plan.md
@@ -0,0 +1,137 @@
+# Bounded Rust browser and telemetry check
+
+This is an **unrun source packet** for the next local step in
+[organization issue 268](https://github.com/durable-workflow/.github/issues/268).
+It follows [Docs #476's accepted main and Pages delivery](https://github.com/durable-workflow/durable-workflow.github.io/pull/476#issuecomment-6647323438).
+General must separately admit the exact preparation and one browser attempt after
+Security review. This file, its manifest and normal CI do not authorize that run.
+No provider, credential, DNS, proxy, TLS, response-header, public telemetry or
+production operation is proposed here.
+
+## Inputs and provenance
+
+`rust-local-browser-packet.json` binds the executable/data closure and exact
+runtime. Its artifact tuple is the accepted SDK #209 input already recorded in
+`rust-policy.json`: artifact 13375154247, ZIP SHA-256
+`ff902191bdb90f7f1d047ecc5d27e9b8c5215a0239e4f11fcbf574f7ddab83d1`,
+484 files / 372 HTML pages / 49 inline-script hashes. Public Rust 4.12.0, crate
+source and tooling identities are preserved. `rust-rule.candidate.json` and all
+policy/approval/browser-result files remain unchanged and disabled/unrun.
+
+The proposed runtime is the existing Playwright 1.64.0 generation, pinned to the
+official **linux/amd64** platform manifest in the packet. Registry metadata was
+read without pulling or running that browser image. `runtime/package-lock.json`
+records the two exact npm archives and integrity values. Only package-lock resolution, advisory metadata and inert source checks were
+performed during this preparation. No
+Playwright package, browser image or browser was executed for the source packet.
+
+After separate admission, prepare a dedicated public dependency directory with
+`runtime/package.json` and its lock. Use `npm ci --ignore-scripts --no-audit
+--no-fund` with `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`. Do not run lifecycle hooks,
+`playwright install`, `npx` downloads or an image build. Verify the approved image
+manifest/config, library version and lock integrity. Mount only its prepared
+`node_modules` directory into the browser cell. No home directory, credentials,
+Docker socket or unrelated repository is an input. An unavailable dependency,
+different platform or runtime mismatch stops the attempt for review.
+
+Before execution, independently verify the ZIP digest and safe regular-file
+membership, recheck the accepted protected-run/deployment identity and compare
+live `__build.json` with the exact artifact. Those are finite read-only checks.
+Stop if the live input has changed, the artifact has expired, or a required input
+is unavailable. Do not dispatch a replacement build or silently retarget the
+packet. The input API expires this ZIP on November 6, 2026 at 00:09:56 UTC.
+
+## What the proposed check exercises
+
+The existing `rust-browser.test.cjs` keeps its 54-page/hash-representative,
+search, history, direct-entry, compact navigation, inline-script and framing
+controls. Its local routing now fulfills verified in-memory artifact buffers
+under the public hostname. It never forwards or fetches a request. Service
+workers are blocked, WebSockets are closed, unknown requests are refused, and
+each context has a finite request budget. `--network=none` is the independent
+container boundary. Using a synthetic HTTPS URL does not exercise real TLS or
+HSTS. Image responses are finite fixtures and do not validate badge providers.
+
+The current artifact has **no promotion DOM in any HTML page**. The real-page
+non-webdriver control therefore requires no promotion request. A distinct,
+labelled synthetic page supplies the expected promotion elements and loads the
+unchanged artifact's `analytics/analytics.js`. Only that fixture exercises the
+existing impression/click handlers. Separate controls retain the webdriver and
+wrong-host refusals, and an unapproved connection must fail CSP.
+
+The controlled non-webdriver getter, native-fetch option observer and prevention
+of external click navigation are harness instrumentation. They do not alter any
+artifact file. Promotion requests must have the exact two-key source/event body,
+POST/text/plain, omitted credentials and no-referrer policy. Authorization and
+cookies are refused for every intercepted request. The Cloudflare beacon module
+and RUM response are **synthetic fixtures**, used only to exercise the loader and
+CSP connection paths. They establish neither vendor-script compatibility nor
+production delivery. No request reaches Cloud or Cloudflare.
+
+Normal CI runs only `rust-browser-source.test.cjs` with fake routes and temporary
+input files, plus the existing header-policy controls. It never imports
+Playwright, launches a browser or runs the optional browser entrypoint.
+
+## Proposed execution and cleanup
+
+Documentation owns one attempt and cleanup. General names the admitted executor
+and host after fresh capacity and competing-work checks. The proposal is one
+native amd64 container, at most **2 CPUs / 2 GiB / 256 PIDs**, two sequential
+tests and at most one active browser. Test deadlines are 150 and 90 seconds;
+the external container deadline is 300 seconds with a ten-second kill grace.
+There is no automatic retry, host IPC, privileged mode, extra capability or
+published port. The writable tmpfs and private shared memory count within the
+container's memory cap. This is a proposed budget, not a reservation.
+
+The following is the exact proposed launch shape after admission. Assign the
+three absolute input paths from the reviewed public-input preparation; reject
+commas/newlines in those paths. `packet_directory` contains only this public
+packet. `runtime_modules` is the prepared public dependency tree. Do not point
+either at a home or credential directory. The pinned image must already exist;
+`--pull=never` prevents an implicit network preparation during the attempt.
+
+```sh
+set -eu
+container_id=
+cleanup() {
+ if [ -n "$container_id" ]; then docker rm --force "$container_id"; fi
+}
+trap cleanup EXIT
+trap 'exit 130' INT
+trap 'exit 143' TERM
+container_id=$(docker create --pull=never --platform linux/amd64 \
+ --name durable-workflow-github-io-rust-csp-telemetry \
+ --init --user pwuser --cpus=2 --memory=2g --pids-limit=256 \
+ --network=none --read-only --cap-drop=ALL \
+ --security-opt=no-new-privileges:true --shm-size=256m \
+ --tmpfs /tmp:rw,size=536870912,mode=1777 \
+ --mount "type=bind,source=$public_artifact_directory,target=/site,readonly" \
+ --mount "type=bind,source=$packet_directory,target=/packet,readonly" \
+ --mount "type=bind,source=$runtime_modules,target=/runtime/node_modules,readonly" \
+ --env NODE_PATH=/runtime/node_modules \
+ mcr.microsoft.com/playwright@sha256:bc72a8df40831ded821e3cbc79c318c21b44d6e51dff9fd89ace7a8249c5b462 \
+ node /packet/rust-browser.test.cjs /site \
+ /packet/rust-rule.candidate.json /packet/rust-inventory.json)
+timeout --signal=TERM --kill-after=10s 300s docker start --attach "$container_id"
+exit_code=$(docker inspect --format '{{.State.ExitCode}}' "$container_id")
+exit "$exit_code"
+```
+
+The cleanup trap removes only the ID created by this invocation; a name collision
+fails rather than deleting an existing container. The admitted executor must
+independently verify absence afterward. A timeout, refused request, changed
+input, browser error, budget violation, missing output or cleanup failure is
+nonpassing evidence. Preserve the original result and stop for review. Do not
+weaken CSP, increase permissions, fetch a new runtime or retry automatically.
+
+Record the exact source commit/tree, packet-file hashes, runtime identities,
+artifact tuple, timestamps, test outputs, request audits, exit/timeout status
+and independent cleanup observations on issue 268. Keep diagnostic logs bounded
+and remove temporary material after review. Do not edit the accepted result
+files to claim a pass before an actual admitted result and independent review.
+Public telemetry, real TLS/HSTS and provider rollout acceptance stay separate.
+
+Runtime references:
+[official image contents and package separation](https://playwright.dev/docs/docker),
+[request interception](https://playwright.dev/docs/network),
+[browser-context routes](https://playwright.dev/docs/api/class-browsercontext#browser-context-route).
From 5c376e42370c03111f665644a367e2bfe5df2f1c Mon Sep 17 00:00:00 2001
From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com>
Date: Thu, 5 Nov 2026 02:03:44 +0000
Subject: [PATCH 2/2] test: clarify exact local origins and browser packet
controls
---
.../browser-headers/rust-browser-inputs.cjs | 20 +++++++++++--------
.../rust-browser-source.test.cjs | 8 ++++++++
.../browser-headers/rust-browser.test.cjs | 2 +-
.../rust-local-browser-packet.json | 6 +++---
4 files changed, 24 insertions(+), 12 deletions(-)
diff --git a/security/browser-headers/rust-browser-inputs.cjs b/security/browser-headers/rust-browser-inputs.cjs
index 9aebc75b840..8ebd526ae6c 100644
--- a/security/browser-headers/rust-browser-inputs.cjs
+++ b/security/browser-headers/rust-browser-inputs.cjs
@@ -71,26 +71,30 @@ function promotionEvent(request) {
assert.match(headers['content-type'] || '', /^text\/plain(?:;|$)/);
assert.ok(typeof request.body === 'string' && request.body.length < 256, 'Promotion body exceeds its bound');
const event = JSON.parse(request.body);
- assert.deepEqual(Object.keys(event).sort(), ['event', 'source']);
+ assert.deepEqual(Object.keys(event).toSorted((left, right) => left.localeCompare(right, 'en')), ['event', 'source']);
assert.equal(event.source, 'sdk-rust-reference');
assert.ok(['impression', 'click'].includes(event.event), 'Unknown promotion event');
return event.event;
}
+function artifactResponse(input, url) {
+ const relative = decodeURIComponent(url.pathname).replace(/^\//, '');
+ if (relative === '__frame_probe') return {kind: 'frame-probe', status: 200, headers: {'content-type': 'text/html'}, body: 'Framing control'};
+ if (relative === '__telemetry_fixture') return {kind: 'promotion-dom-fixture', status: 200, headers: {...input.headers, 'content-type': 'text/html'}, body: 'Synthetic promotion control'};
+ const file = [relative, relative.replace(/\/$/, '') + '/index.html', relative ? null : 'index.html'].find((name) => name && input.files.has(name));
+ if (!file) return {kind: 'missing', status: 404, body: ''};
+ return {kind: 'artifact', status: 200, headers: {...input.headers, 'content-type': MIME[path.extname(file)] || 'application/octet-stream', 'cache-control': 'no-store'}, body: input.files.get(file)};
+}
+
function localResponse(input, request, origin = ORIGIN) {
- assert.ok([ORIGIN, 'https://example.invalid'].includes(origin), 'Unexpected local origin');
+ if (origin !== ORIGIN && origin !== 'https://example.invalid') throw new Error('Unexpected local origin');
const requestHeaders = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));
assert.ok(!Object.hasOwn(requestHeaders, 'authorization'), 'Local requests cannot carry authorization');
assert.ok(!Object.hasOwn(requestHeaders, 'cookie'), 'Local requests cannot carry cookies');
const url = new URL(request.url);
if (url.origin === origin) {
assert.equal(request.method, 'GET', 'Artifact requests must be GET');
- const relative = decodeURIComponent(url.pathname).replace(/^\//, '');
- if (relative === '__frame_probe') return {kind: 'frame-probe', status: 200, headers: {'content-type': 'text/html'}, body: 'Framing control'};
- if (relative === '__telemetry_fixture') return {kind: 'promotion-dom-fixture', status: 200, headers: {...input.headers, 'content-type': 'text/html'}, body: 'Synthetic promotion control'};
- const file = [relative, relative.replace(/\/$/, '') + '/index.html', relative ? null : 'index.html'].find((name) => name && input.files.has(name));
- if (!file) return {kind: 'missing', status: 404, body: ''};
- return {kind: 'artifact', status: 200, headers: {...input.headers, 'content-type': MIME[path.extname(file)] || 'application/octet-stream', 'cache-control': 'no-store'}, body: input.files.get(file)};
+ return artifactResponse(input, url);
}
const cors = {'access-control-allow-origin': origin};
if (request.url === BEACON && request.method === 'GET') {
diff --git a/security/browser-headers/rust-browser-source.test.cjs b/security/browser-headers/rust-browser-source.test.cjs
index 8bc8133b815..f3ff6430fd9 100644
--- a/security/browser-headers/rust-browser-source.test.cjs
+++ b/security/browser-headers/rust-browser-source.test.cjs
@@ -68,6 +68,14 @@ test('promotion fixtures require exact method, event shape and credential absenc
]) assert.throws(() => localResponse(input, {...valid, ...change}));
});
+test('local-origin selection refuses deceptive hosts and added URL components', (t) => {
+ const value = fixture(t);
+ const input = loadInputs(value.root, value.packet, value.inventory);
+ for (const origin of [ORIGIN + '.invalid', ORIGIN + '@unreviewed.invalid', 'https://unreviewed.invalid/' + ORIGIN, ORIGIN + ':444', ORIGIN + '/path', 'http://rust.durable-workflow.com']) {
+ assert.throws(() => localResponse(input, request(ORIGIN + '/'), origin), /Unexpected local origin/);
+ }
+});
+
test('fake routing fulfills or aborts without a network continuation', async (t) => {
const value = fixture(t);
const input = loadInputs(value.root, value.packet, value.inventory);
diff --git a/security/browser-headers/rust-browser.test.cjs b/security/browser-headers/rust-browser.test.cjs
index 32ee55b0276..76486a8efbe 100644
--- a/security/browser-headers/rust-browser.test.cjs
+++ b/security/browser-headers/rust-browser.test.cjs
@@ -5,8 +5,8 @@ const path = require('node:path');
const test = require('node:test');
const {ORIGIN, PROMOTION, verifyPacketFiles, loadInputs, installLocalRoutes} = require('./rust-browser-inputs.cjs');
const reviewedPacket = verifyPacketFiles(__dirname);
-assert.equal(require('playwright/package.json').version, reviewedPacket.playwright_version);
const {chromium} = require('playwright');
+test.before(() => assert.equal(require('playwright/package.json').version, reviewedPacket.playwright_version));
test('Rust artifact CSP and navigation compatibility', {timeout: 150000}, async () => {
const root = path.resolve(process.argv[2]);
diff --git a/security/browser-headers/rust-local-browser-packet.json b/security/browser-headers/rust-local-browser-packet.json
index f9557dd2678..f7c0422d199 100644
--- a/security/browser-headers/rust-local-browser-packet.json
+++ b/security/browser-headers/rust-local-browser-packet.json
@@ -24,9 +24,9 @@
"max_active_browsers": 1
},
"files": {
- "rust-browser.test.cjs": "9a9854690a585d5589c17a99b6a92e83b7797289abe3aa3c776268d8612c07ae",
- "rust-browser-inputs.cjs": "d3ed02b8e6d392b472e3e893a8464cb1ab4255be49abaf5ac262da8b71aee422",
- "rust-browser-source.test.cjs": "42c86ac5eac34b71388ac4f0943ca55000fafa31243304a7b2f317a7f55be55c",
+ "rust-browser.test.cjs": "0b2f731db13e4f095230673bfda3d97a36f71c5ef62b8f3702a7978740d17d61",
+ "rust-browser-inputs.cjs": "43bb050185f05187e7d94a40d73b4fae965276696fe7356e6609b319c6b120ee",
+ "rust-browser-source.test.cjs": "deafc5ce08806c148b189a48f11012cc0c03600122dd8fe34eb01e223f381952",
"rust-policy.json": "a357bcc5ea8ad96db5e601e4e1c1cb3c8c7eb3441caa9b331d0c2a906ffe84d8",
"rust-inventory.json": "8c4bc2eb1ef3ffeeb9788a9d0b44d7b718a2a56caff55b3d4531716a25b270d7",
"rust-rule.candidate.json": "bce0c66db4454bc16941a8f5dadb04706315e673d5e2c33f677e7728bafd0135",