#!/usr/bin/env bash set -euo pipefail repository_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) guard="$repository_root/tools/require-base-config.sh" terminal_url=https://terminal.invalid/rpc public_url=https://public.invalid/rpc account=demo-account sender=0x1111111111111111111111111111111111111111 recipient=0x2222222222222222222222222222222222222222 run_guard() { local mode=${1-deploy} shift || true env -i PATH="$PATH" \ BASE_SEPOLIA_RPC_URL="$terminal_url" \ BASE_SEPOLIA_PUBLIC_RPC_URL="$public_url" \ BASE_SEPOLIA_ACCOUNT="$account" \ BASE_SEPOLIA_SENDER="$sender" \ BASE_SEPOLIA_RECIPIENT="$recipient" \ "$@" bash "$guard" "$mode" } assert_no_config_values() { local output=$1 shift local value for value in "$terminal_url" "$public_url" "$account" "$sender" "$recipient" "$@"; do [[ -n $value ]] || continue if [[ $output == *"$value"* ]]; then echo "configuration guard leaked a configured value" >&2 exit 1 fi done } expect_rejected() { local assignment=$1 local output if output=$(run_guard deploy "$assignment" 2>&1); then echo "configuration guard accepted invalid fixture: ${assignment%%=*}" >&2 exit 1 fi assert_no_config_values "$output" "${assignment#*=}" } for mode in deploy upgrade transfer; do output=$(run_guard "$mode" 2>&1) [[ -z $output ]] || { echo "configuration guard produced output for valid fixtures" >&2; exit 1; } done for assignment in \ BASE_SEPOLIA_RPC_URL= \ BASE_SEPOLIA_PUBLIC_RPC_URL= \ BASE_SEPOLIA_ACCOUNT= \ BASE_SEPOLIA_SENDER= \ BASE_SEPOLIA_RECIPIENT= \ BASE_SEPOLIA_ACCOUNT='bad account' \ BASE_SEPOLIA_SENDER=0x1234 \ BASE_SEPOLIA_RECIPIENT=0x0000000000000000000000000000000000000000 \ BASE_SEPOLIA_RECIPIENT="$sender" \ BASE_SEPOLIA_RPC_URL=http://terminal.invalid \ BASE_SEPOLIA_PUBLIC_RPC_URL=http://public.invalid \ BASE_SEPOLIA_PUBLIC_RPC_URL=https://user@public.invalid \ BASE_SEPOLIA_PUBLIC_RPC_URL='https://public.invalid/?api_key=fixture' \ BASE_SEPOLIA_PUBLIC_RPC_URL='https://public.invalid/?token=fixture'; do expect_rejected "$assignment" done private_key_name=PRIVATE_$(printf KEY) mnemonic_name=MNEM$(printf ONIC) fake_key=0x$(printf 'a%.0s' {1..64}) output=$( env -i PATH="$PATH" "$private_key_name=$fake_key" "$mnemonic_name=fixture words only" \ bash "$guard" deploy 2>&1 || true ) if [[ $output != *BASE_SEPOLIA_RPC_URL* ]]; then echo "configuration guard unexpectedly accepted a raw-key or mnemonic fallback" >&2 exit 1 fi assert_no_config_values "$output" dry_run=$( make -n -C "$repository_root" deploy-base-sepolia \ BASE_SEPOLIA_RPC_URL=https://terminal.invalid \ BASE_SEPOLIA_PUBLIC_RPC_URL=https://public.invalid \ BASE_SEPOLIA_ACCOUNT=demo \ BASE_SEPOLIA_SENDER=0x1111111111111111111111111111111111111111 \ BASE_SEPOLIA_RECIPIENT=0x2222222222222222222222222222222222222222 ) [[ $dry_run == *'--account "demo"'* ]] || { echo "Base dry run omitted --account" >&2; exit 1; } [[ $dry_run == *'--sender "0x1111111111111111111111111111111111111111"'* ]] || \ { echo "Base dry run omitted --sender" >&2; exit 1; } [[ $dry_run == *'--slow'* ]] || { echo "Base dry run omitted --slow" >&2; exit 1; } if [[ ${dry_run,,} == *private-key* || ${dry_run,,} == *mnemonic* ]]; then echo "Base dry run exposed a raw signing option" >&2 exit 1 fi node --input-type=module - "$repository_root" <<'NODE' import assert from "node:assert/strict"; import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; const repositoryRoot = process.argv[2]; const { archiveManifest, selectManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/select-manifest.mjs"))); const { validateManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/finalize-manifest.mjs"))); const { publishManifest, validatePublicManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/publish-web-manifest.mjs"))); const root = await mkdtemp(join(tmpdir(), "uups-base-config-")); const deployments = join(root, "deployments"); const webManifest = join(root, "web/public/deployment.json"); const owner = "0x1111111111111111111111111111111111111111"; const recipient = "0x2222222222222222222222222222222222222222"; const base = { schemaVersion: 1, network: "baseSepolia", chainId: 84532, deploymentBlock: 99, rpcUrl: "https://public.invalid/rpc", explorerBaseUrl: "https://sepolia.basescan.org", token: "0x3333333333333333333333333333333333333333", proxy: "0x4444444444444444444444444444444444444444", implementation: "0x5555555555555555555555555555555555555555", owner, actors: [ { label: "Presenter", address: owner }, { label: "Recipient", address: recipient }, ], }; const anvil = { ...base, network: "anvil", chainId: 31337, rpcUrl: "http://127.0.0.1:8545", explorerBaseUrl: undefined, actors: [ { label: "owner", address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, { label: "Alice", address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" }, { label: "Bob", address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" }, ], owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", }; delete anvil.explorerBaseUrl; try { await mkdir(deployments, { recursive: true }); await writeFile(join(deployments, "base-sepolia.json"), `${JSON.stringify(base)}\n`); await writeFile(join(deployments, "anvil.json"), `${JSON.stringify(anvil)}\n`); await selectManifest({ root, network: "baseSepolia" }); const activeBefore = await readFile(join(deployments, "active.json")); const anvilBefore = await readFile(join(deployments, "anvil.json")); const published = await publishManifest({ activePath: join(deployments, "active.json"), outputPath: webManifest, }); assert.equal(published.rpcUrl, "https://public.invalid/rpc"); assert.equal(published.explorerBaseUrl, "https://sepolia.basescan.org"); assert.equal((await readFile(webManifest, "utf8")).includes("terminal.invalid"), false); const without = (field) => { const manifest = { ...base }; delete manifest[field]; return manifest; }; const invalidCases = [ ["missing rpcUrl", without("rpcUrl"), /rpcUrl/], ["missing explorerBaseUrl", without("explorerBaseUrl"), /explorerBaseUrl/], ["wrong actor count", { ...base, actors: [base.actors[0]] }, /exactly Presenter and Recipient|actor configuration/], ["wrong actor labels", { ...base, actors: [{ ...base.actors[0], label: "Owner" }, base.actors[1]] }, /Presenter and Recipient|actor configuration/], ["owner is not Presenter", { ...base, owner: recipient }, /owner.*Presenter|first actor/], ["HTTP public RPC", { ...base, rpcUrl: "http://public.invalid/rpc" }, /HTTPS/], ["credential-bearing public RPC", { ...base, rpcUrl: "https://user@public.invalid/rpc" }, /credentials|prohibited secret/], ["wrong BaseScan root", { ...base, explorerBaseUrl: "https://example.invalid" }, /BaseScan/], ]; for (const [label, invalid, rejection] of invalidCases) { assert.throws(() => validateManifest(invalid), rejection, `confirmation accepted ${label}`); assert.throws(() => validatePublicManifest(invalid), rejection, `publication validation accepted ${label}`); await writeFile(join(deployments, "active.json"), `${JSON.stringify(invalid)}\n`); await assert.rejects( () => publishManifest({ activePath: join(deployments, "active.json"), outputPath: webManifest }), rejection, `publication accepted ${label}`, ); } await writeFile(join(deployments, "active.json"), activeBefore); const archived = await archiveManifest({ root, network: "baseSepolia", now: new Date("2026-08-21T12:34:56.789Z"), }); assert.equal(archived.path, join(deployments, "base-sepolia.20260821T123456789Z.json")); await access(archived.path); await assert.rejects(() => access(join(deployments, "base-sepolia.json"))); assert.deepEqual(await readFile(join(deployments, "active.json")), activeBefore); assert.deepEqual(await readFile(join(deployments, "anvil.json")), anvilBefore); const canonicalPath = join(deployments, "base-sepolia.json"); const canonicalBytes = Buffer.from(`${JSON.stringify(base)}\n`); const existingBytes = Buffer.from("existing archive bytes\n"); const collisionDate = new Date("2026-08-21T12:35:56.789Z"); const collisionPath = join(deployments, "base-sepolia.20260821T123556789Z.json"); await writeFile(canonicalPath, canonicalBytes); await writeFile(collisionPath, existingBytes); await assert.rejects( () => archiveManifest({ root, network: "baseSepolia", now: collisionDate }), /existing Base Sepolia archive/, ); assert.deepEqual(await readFile(canonicalPath), canonicalBytes); assert.deepEqual(await readFile(collisionPath), existingBytes); const boundaryDate = new Date("2026-08-21T12:36:56.789Z"); const boundaryPath = join(deployments, "base-sepolia.20260821T123656789Z.json"); const boundaryBytes = Buffer.from("archive created by racing process\n"); await assert.rejects( () => archiveManifest({ root, network: "baseSepolia", now: boundaryDate, io: { link: async (_source, target) => { await writeFile(target, boundaryBytes); const error = new Error("collision at link boundary"); error.code = "EEXIST"; throw error; }, }, }), /existing Base Sepolia archive/, ); assert.deepEqual(await readFile(canonicalPath), canonicalBytes); assert.deepEqual(await readFile(boundaryPath), boundaryBytes); await assert.rejects(() => archiveManifest({ root, network: "anvil" }), /baseSepolia/); } finally { await rm(root, { recursive: true, force: true }); } NODE echo "Base configuration and manifest filesystem tests passed"