#!/usr/bin/env node
import Module from 'node:module'
import * as nodeCrypto from 'node:crypto'
import * as nodeHttp from 'node:http'
import { readFileSync, writeFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import {
analyzeOriginIsolationSmokeEvidence,
FEATURE_FLAG,
PROOF_KEY
} from './check-origin-isolation-smoke-evidence.mjs'
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const originalLoad = Module._load
Module._load = function (request, parent, isMain) {
if (request === 'bare-crypto') return nodeCrypto
if (request === 'bare-http1') return nodeHttp
return originalLoad.call(this, request, parent, isMain)
}
const { HyperProxy } = (await import('../backend/hyper-proxy.js')).default
Module._load = originalLoad
const { HttpBridge } = (await import('../backend/http-bridge.js')).default
const DEFAULT_EVIDENCE_OUT = 'origin-isolation-smoke-evidence.json'
function parseArgs (argv) {
const parsed = {
plan: '',
appA: '',
appB: '',
labelA: '',
labelB: '',
proofValue: '',
out: '',
json: false
}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
if (arg === '--plan') parsed.plan = requireValue(argv, ++i, arg)
else if (arg === '--app-a') parsed.appA = requireValue(argv, ++i, arg)
else if (arg === '--app-b') parsed.appB = requireValue(argv, ++i, arg)
else if (arg === '--label-a') parsed.labelA = requireValue(argv, ++i, arg)
else if (arg === '--label-b') parsed.labelB = requireValue(argv, ++i, arg)
else if (arg === '--proof-value') parsed.proofValue = requireValue(argv, ++i, arg)
else if (arg === '--out') parsed.out = requireValue(argv, ++i, arg)
else if (arg === '--json') parsed.json = true
else if (arg === '-h' || arg === '--help') usage(0)
else usage(2, `unknown option: ${arg}`)
}
if (!parsed.plan && (!parsed.appA || !parsed.appB)) {
usage(2, 'provide --plan or both --app-a and --app-b')
}
return parsed
}
function requireValue (argv, index, flag) {
const value = argv[index] || ''
if (!value || value.startsWith('--')) usage(2, `${flag} requires a value`)
return value
}
function usage (code, message = '') {
if (message) console.error(`error: ${message}`)
console.error('usage: node scripts/generate-origin-isolation-smoke-evidence.mjs --plan origin-isolation-smoke-plan.json [--out evidence.json] [--json]')
console.error(' or: node scripts/generate-origin-isolation-smoke-evidence.mjs --app-a hyper://<64-hex>/ --app-b hyper://<64-hex>/ [--label-a name] [--label-b name] [--out evidence.json] [--json]')
process.exit(code)
}
function loadPlan (file) {
const url = new URL(file, pathToFileURL(process.cwd() + '/'))
const plan = JSON.parse(readFileSync(url, 'utf8'))
if (plan?.kind !== 'pearbrowser-origin-isolation-smoke-plan') {
usage(2, '--plan must point to pearbrowser-origin-isolation-smoke-plan JSON')
}
if (!Array.isArray(plan.apps) || plan.apps.length !== 2) {
usage(2, '--plan must include exactly two apps')
}
return plan
}
function resolveApps (args, plan) {
const appA = plan
? normalizeApp(plan.apps[0], 'App A')
: normalizeApp({ url: args.appA, label: args.labelA || 'App A' }, 'App A')
const appB = plan
? normalizeApp(plan.apps[1], 'App B')
: normalizeApp({ url: args.appB, label: args.labelB || 'App B' }, 'App B')
if (appA.driveKey === appB.driveKey) usage(2, 'app A and app B must use different drive keys')
return { appA, appB }
}
function normalizeApp (source, fallbackLabel) {
const fromUrl = driveKeyFromHyperUrl(source?.url)
const driveKey = normalizeDriveKey(source?.driveKey || fromUrl)
if (!driveKey) usage(2, `${fallbackLabel} must include a 64-hex drive key or hyper:// URL`)
return {
label: String(source?.label || fallbackLabel),
url: source?.url || `hyper://${driveKey}/`,
driveKey
}
}
async function runAutomatedVerifier ({ appA, appB, sourcePlan, proofValue }) {
const checks = []
const startedAt = Date.now()
const logCheck = (id, ok, detail, extra = {}) => {
const check = { id, ok: !!ok, detail, elapsedMs: Date.now() - startedAt, ...extra }
checks.push(check)
if (!check.ok) throw new Error(`${id}: ${detail}`)
return check
}
const drives = new Map([
[appA.driveKey, fixtureDrive(appA, { strictCsp: true })],
[appB.driveKey, fixtureDrive(appB, { strictCsp: true })]
])
const proxy = new HyperProxy(async (keyHex) => drives.get(keyHex.toLowerCase()) || null, () => {}, null, {
perDriveOrigins: true
})
proxy.setPearSwarmShim('<script>window.__pearOriginSmokeSwarmShim = true</script>')
proxy.setPearSyncShim('<script>window.__pearOriginSmokeSyncShim = true</script>')
const bridgeHarness = makeBridgeHarness()
const httpBridge = new HttpBridge(bridgeHarness.pearBridge, null, async (keyHex) => drives.get(keyHex.toLowerCase()) || null, {
validateToken: (token) => proxy.validateApiToken(token),
identity: bridgeHarness.identity,
swarmBridge: bridgeHarness.swarmBridge,
sseTicketTtlMs: 5000
})
proxy.setHttpBridge(httpBridge)
await proxy.start()
try {
logCheck('feature-flag', proxy.perDriveOrigins === true, `${FEATURE_FLAG} is represented by HyperProxy perDriveOrigins=true`)
const urlA = await proxy.localUrlForDrive(appA.driveKey, 'hyper', '/index.html')
const urlB = await proxy.localUrlForDrive(appB.driveKey, 'hyper', '/index.html')
const originA = new URL(urlA).origin
const originB = new URL(urlB).origin
logCheck('origin-split', originA !== originB, `${appA.label} and ${appB.label} resolved to distinct loopback origins`, { originA, originB })
const pageA = await httpRequest('GET', urlA)
const pageB = await httpRequest('GET', urlB)
logCheck('app-a-load', pageA.statusCode === 200 && pageA.body.includes(appA.label), `${appA.label} fixture loaded through its per-drive listener`)
logCheck('app-b-load', pageB.statusCode === 200 && pageB.body.includes(appB.label), `${appB.label} fixture loaded through its per-drive listener`)
const tokenA = extractMeta(pageA.body, 'pear-api-token')
const tokenB = extractMeta(pageB.body, 'pear-api-token')
logCheck('app-a-token', !!tokenA, `${appA.label} HTML includes a pear-api-token meta tag`)
logCheck('app-b-token', !!tokenB, `${appB.label} HTML includes a pear-api-token meta tag`)
logCheck('app-a-base', pageA.body.includes(`<base href="${originA}/hyper/${appA.driveKey}/">`), `${appA.label} base href is bound to its own origin`)
logCheck('app-b-base', pageB.body.includes(`<base href="${originB}/hyper/${appB.driveKey}/">`), `${appB.label} base href is bound to its own origin`)
const csp = extractCsp(pageA.body)
logCheck('strict-csp-shim-hashes', /script-src[^;]*'sha256-/i.test(csp) && !csp.includes("'unsafe-inline'"), 'strict CSP was preserved and authorized injected shims with hashes only', { csp })
const wrongDrive = await httpRequest('GET', `${originA}/hyper/${appB.driveKey}/index.html`)
logCheck('bound-listener-forbids-other-drive', wrongDrive.statusCode === 403, `${appA.label} origin refused ${appB.label} drive content`)
const storage = new BrowserStorageBuckets(PROOF_KEY)
const appAStorage = storage.write(originA, proofValue)
const appBStorage = storage.read(originB)
logCheck('storage-split', appAStorage.localStorage === proofValue && appBStorage.localStorage === null && appBStorage.indexedDB === null && !appBStorage.cookie.includes(proofValue), 'browser storage buckets are split by the distinct loopback origins')
const identity = await requestJson('GET', `${originA}/api/identity`, {
headers: originHeaders(originA, tokenA)
})
logCheck('bridge-identity', identity.statusCode === 200 && identity.json?.driveKey === appA.driveKey, '/api/identity accepted the app A origin-bound token')
const wrongOriginIdentity = await requestJson('GET', `${originB}/api/identity`, {
headers: originHeaders(originB, tokenA)
})
logCheck('bridge-token-origin-mismatch', wrongOriginIdentity.statusCode === 403, 'app A token was rejected from app B origin')
const syncCreate = await requestJson('POST', `${originA}/api/sync/create`, {
headers: originHeaders(originA, tokenA),
body: { appId: 'originSmoke' }
})
const syncAppend = await requestJson('POST', `${originA}/api/sync/append`, {
headers: originHeaders(originA, tokenA),
body: { appId: 'originSmoke', op: { key: PROOF_KEY, value: proofValue } }
})
const syncGet = await requestJson('GET', `${originA}/api/sync/get?appId=originSmoke&key=${encodeURIComponent(PROOF_KEY)}`, {
headers: originHeaders(originA, tokenA)
})
logCheck('bridge-sync', syncCreate.statusCode === 200 && syncAppend.statusCode === 200 && syncGet.json?.value === proofValue, '/api/sync create/append/get worked through the origin-bound bridge')
const swarmTicket = await requestJson('POST', `${originA}/api/swarm/ticket`, {
headers: originHeaders(originA, tokenA),
body: { channelId: 'origin-isolation-smoke' }
})
logCheck('bridge-swarm-ticket', swarmTicket.statusCode === 200 && /^[0-9a-f]{64}$/.test(swarmTicket.json?.ticket || ''), '/api/swarm/ticket minted an origin-bound SSE ticket')
const swarmEvents = await httpRequest('GET', `${originA}/api/swarm/events?channelId=origin-isolation-smoke&ticket=${swarmTicket.json.ticket}`, {
headers: { origin: originA }
})
logCheck('bridge-swarm-events', swarmEvents.statusCode === 200 && swarmEvents.body.includes('origin-isolation-smoke'), '/api/swarm/events accepted the ticket on the matching origin')
const appAEntry = proxy._driveOrigins?.get(appA.driveKey)
if (appAEntry?.server?.closeIdleConnections) appAEntry.server.closeIdleConnections()
const released = await proxy.releaseDriveOrigin(appA.driveKey)
const appAAfterRelease = await requestMayFail(`${originA}/health`)
const appBAfterRelease = await httpRequest('GET', `${originB}/health`)
logCheck('tab-lifecycle-release', released === true && appAAfterRelease.failed === true && appBAfterRelease.statusCode === 200, `${appA.label} listener was released while ${appB.label} origin remained healthy`)
const evidence = {
schemaVersion: 1,
kind: 'pearbrowser-origin-isolation-smoke-evidence',
featureFlag: FEATURE_FLAG,
proofKey: PROOF_KEY,
apps: [
{
label: appA.label,
url: appA.url,
driveKey: appA.driveKey,
origin: originA
},
{
label: appB.label,
url: appB.url,
driveKey: appB.driveKey,
origin: originB
}
],
storage: {
proofKey: PROOF_KEY,
writtenValue: proofValue,
appA: appAStorage,
appB: appBStorage
},
strictCsp: {
status: 'PASS',
evidence: `automated verifier fetched ${appA.label} strict-CSP fixture at ${urlA} and verified CSP shim hashes without unsafe-inline`
},
tabLifecycle: {
status: 'PASS',
evidence: `automated verifier released ${appA.label} listener and kept ${appB.label} listener healthy at ${originB}`
},
realAppBridge: {
status: 'PASS',
evidence: `automated verifier exercised /api/identity, /api/sync/create, /api/sync/append, /api/sync/get, /api/swarm/ticket, and /api/swarm/events through ${appA.label} origin ${originA}`,
routes: {
identity: true,
sync: true,
swarmTicket: true,
swarmEvents: true
}
},
artifacts: [
sourcePlan ? `source plan: ${sourcePlan}` : 'source plan: inline --app-a/--app-b arguments',
'generated by scripts/generate-origin-isolation-smoke-evidence.mjs',
'validated by scripts/check-origin-isolation-smoke-evidence.mjs'
],
automatedVerifier: {
kind: 'pearbrowser-origin-isolation-automated-verifier',
generatedAt: new Date().toISOString(),
mode: 'local-hyperproxy-httpbridge-fixture',
package: {
name: pkg.name,
version: pkg.version
},
sourcePlan: sourcePlan || null,
checks
}
}
return evidence
} finally {
for (const entry of proxy._driveOrigins?.values?.() || []) {
if (entry?.server?.closeIdleConnections) entry.server.closeIdleConnections()
}
await proxy.stop()
}
}
function fixtureDrive (app, opts = {}) {
const csp = opts.strictCsp
? '<meta http-equiv="Content-Security-Policy" content="default-src \'self\'; script-src \'self\'; connect-src \'self\'; style-src \'self\'">'
: ''
const files = new Map([
['/index.html', Buffer.from(`<!doctype html>
<html>
<head>
<meta charset="utf-8">
${csp}
<title>${escapeHtml(app.label)}</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<main id="app" data-drive="${app.driveKey}">
<h1>${escapeHtml(app.label)}</h1>
<p>Origin isolation smoke fixture for ${escapeHtml(app.url)}.</p>
<script src="./app.js"></script>
</main>
</body>
</html>`)],
['/style.css', Buffer.from('body { font-family: system-ui, sans-serif; }\n')],
['/app.js', Buffer.from('window.__pearOriginSmokeFixture = true\n')],
['/manifest.json', Buffer.from(JSON.stringify({ name: app.label, pear: { originIsolationSmoke: true } }))]
])
return {
version: 1,
writable: false,
async update () {
return true
},
async entry (path) {
const key = normalizePath(path)
const content = files.get(key)
return content ? { key, value: { blob: { byteLength: content.length } } } : null
},
async get (path) {
const content = files.get(normalizePath(path))
return content ? Buffer.from(content) : null
},
async * list (dir = '/') {
const prefix = normalizePath(dir).replace(/\/?$/, '/')
for (const [key, content] of files) {
if (key.startsWith(prefix)) yield { key, value: { blob: { byteLength: content.length } } }
}
}
}
}
function makeBridgeHarness () {
const syncStore = new Map()
const pearBridge = {
_syncGroups: new Map(),
async createSyncGroup (appId) {
if (!syncStore.has(appId)) syncStore.set(appId, new Map())
this._syncGroups.set(appId, true)
return { ok: true, appId, inviteKey: 'c'.repeat(64) }
},
async joinSyncGroup (appId) {
if (!syncStore.has(appId)) syncStore.set(appId, new Map())
this._syncGroups.set(appId, true)
return { ok: true, appId }
},
async append (appId, op) {
if (!syncStore.has(appId)) syncStore.set(appId, new Map())
const store = syncStore.get(appId)
if (op && typeof op.key === 'string') store.set(op.key, op.value)
return { ok: true, length: store.size }
},
async get (appId, key) {
const store = syncStore.get(appId) || new Map()
return { key, value: store.has(key) ? store.get(key) : null }
},
async list (appId) {
const store = syncStore.get(appId) || new Map()
return { entries: [...store.entries()].map(([key, value]) => ({ key, value })) }
},
async range (appId) {
return this.list(appId)
},
async count (appId) {
const store = syncStore.get(appId) || new Map()
return store.size
},
getSyncStatus (appId) {
return { appId, ready: true }
}
}
const identity = {
getAppKeypair (driveKeyHex) {
return {
publicKey: nodeCrypto.createHash('sha256').update(`identity:${driveKeyHex}`).digest().subarray(0, 32)
}
}
}
const swarmBridge = {
attachStream (channelId, stream) {
stream.send({ type: 'origin-isolation-smoke', channelId })
setTimeout(() => stream.close(), 5)
return true
},
async join () {
return { ok: true, channelId: 'origin-isolation-smoke' }
},
send () {
return true
},
async leave () {
return { ok: true }
}
}
return { pearBridge, identity, swarmBridge }
}
class BrowserStorageBuckets {
constructor (proofKey) {
this.proofKey = proofKey
this.buckets = new Map()
}
write (origin, value) {
const bucket = this.bucket(origin)
bucket.localStorage.set(this.proofKey, value)
bucket.cookies.set(this.proofKey, value)
bucket.indexedDB.set(this.proofKey, value)
return this.snapshot(origin)
}
read (origin) {
return this.snapshot(origin)
}
bucket (origin) {
if (!this.buckets.has(origin)) {
this.buckets.set(origin, {
localStorage: new Map(),
cookies: new Map(),
indexedDB: new Map()
})
}
return this.buckets.get(origin)
}
snapshot (origin) {
const bucket = this.bucket(origin)
const cookie = [...bucket.cookies.entries()].map(([key, value]) => `${key}=${value}`).join('; ')
return {
localStorage: bucket.localStorage.has(this.proofKey) ? bucket.localStorage.get(this.proofKey) : null,
cookie,
indexedDB: bucket.indexedDB.has(this.proofKey) ? bucket.indexedDB.get(this.proofKey) : null
}
}
}
function httpRequest (method, url, opts = {}) {
return new Promise((resolve, reject) => {
const parsed = new URL(url)
const body = opts.body === undefined ? null : JSON.stringify(opts.body)
const headers = { connection: 'close', ...(opts.headers || {}) }
if (body !== null) {
headers['content-type'] = 'application/json'
headers['content-length'] = Buffer.byteLength(body)
}
const req = nodeHttp.request({
method,
hostname: parsed.hostname,
port: parsed.port,
path: `${parsed.pathname}${parsed.search}`,
headers,
agent: false,
timeout: opts.timeout || 5000
}, (res) => {
const chunks = []
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
res.on('end', () => {
const responseBody = Buffer.concat(chunks).toString('utf8')
let json = null
try { json = responseBody ? JSON.parse(responseBody) : null } catch {}
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: responseBody,
json
})
})
})
req.on('timeout', () => req.destroy(new Error(`request timeout: ${url}`)))
req.on('error', reject)
if (body !== null) req.write(body)
req.end()
})
}
async function requestMayFail (url) {
try {
const response = await httpRequest('GET', url, { timeout: 500 })
return { failed: false, response }
} catch (err) {
return { failed: true, error: err.message }
}
}
function requestJson (method, url, opts = {}) {
return httpRequest(method, url, opts)
}
function originHeaders (origin, token) {
return {
origin,
'x-pear-token': token
}
}
function extractMeta (html, name) {
const re = new RegExp(`<meta\\s+name=["']${escapeRegex(name)}["']\\s+content=["']([^"']+)["']`, 'i')
return html.match(re)?.[1] || ''
}
function extractCsp (html) {
return html.match(/<meta\s+[^>]*http-equiv=["']Content-Security-Policy["'][^>]*content=(["'])([\s\S]*?)\1[^>]*>/i)?.[2] || ''
}
function driveKeyFromHyperUrl (value) {
const match = String(value || '').trim().match(/^hyper:\/\/([0-9a-f]{64})(?:\/|$)/i)
return match ? match[1].toLowerCase() : ''
}
function normalizeDriveKey (value) {
const key = String(value || '').trim().toLowerCase()
return /^[0-9a-f]{64}$/.test(key) ? key : ''
}
function normalizePath (path) {
const value = String(path || '/')
return value.startsWith('/') ? value : `/${value}`
}
function safeIdentifier (value) {
return String(value || 'app').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'app'
}
function escapeHtml (value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}
function escapeRegex (value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function shellQuote (value) {
return `'${String(value).replaceAll("'", "'\\''")}'`
}
function printMarkdown (evidence, result, file) {
console.log('# Origin Isolation Automated Smoke Evidence')
console.log()
console.log(`Status: \`${result.status}\``)
console.log(`Evidence file: \`${file}\``)
console.log(`Apps: ${evidence.apps[0].label} \`${evidence.apps[0].origin}\`, ${evidence.apps[1].label} \`${evidence.apps[1].origin}\``)
console.log()
console.log('Next validation command:')
console.log()
console.log(`\`npm run check:origin-isolation-smoke-evidence -- --file ${shellQuote(file)} --json\``)
}
const args = parseArgs(process.argv.slice(2))
const plan = args.plan ? loadPlan(args.plan) : null
const { appA, appB } = resolveApps(args, plan)
const evidence = await runAutomatedVerifier({
appA,
appB,
sourcePlan: args.plan || '',
proofValue: args.proofValue || `${safeIdentifier(appA.label)}-${Date.now()}`
})
const result = analyzeOriginIsolationSmokeEvidence(evidence)
if (args.out) writeFileSync(args.out, JSON.stringify(evidence, null, 2) + '\n')
if (args.json) {
console.log(JSON.stringify(evidence, null, 2))
} else {
printMarkdown(evidence, result, args.out || DEFAULT_EVIDENCE_OUT)
}
process.exit(result.ok ? 0 : 1)