Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / web / src / runtime.ts
web/src/runtime.ts82 lines · 3846 bytes
1// This run's settings, from config.ts plus the URL (browser side).
2import { CONFIG, type AppMode, type ChainConfig } from './config'
3
4const params = new URLSearchParams(typeof location === 'undefined' ? '' : location.search)
5
6export function pickMode(raw: string | null): AppMode {
7 return raw === 'offline' || raw === 'airlock' ? raw : CONFIG.defaultMode
8}
9
10export const MODE: AppMode = pickMode(params.get('mode'))
11
12/** Offline playback: no fetch, no WebSocket; everything comes from src/recorded-run.json */
13export const OFFLINE = MODE === 'offline'
14
15/** ?rpc= override: must be an http(s) URL, otherwise ignored */
16export function parseRpcOverride(raw: string | null | undefined): string | null {
17 if (!raw) return null
18 const s = raw.trim()
19 if (!/^https?:\/\/[^\s/?#]+(?:[/?#]\S*)?$/i.test(s)) return null
20 return s.replace(/\/+$/, '')
21}
22
23/** Loopback address (local fork): 127.0.0.1, localhost, [::1] */
24export function isLoopback(url: string): boolean {
25 return /^https?:\/\/(?:127\.\d+\.\d+\.\d+|localhost|\[::1\])(?::\d+)?(?:[/?#]|$)/i.test(url)
26}
27
28/** The ?rpc= override this page uses: a local fork (a loopback address) only. Any other host is ignored, so a link to
29 * the public site can never make it read someone else's JSON-RPC and draw that under "Live · Ethereum Sepolia" with
30 * Etherscan links. Offline mode has no endpoints at all. */
31export function rpcOverrideOf(raw: string | null | undefined, offline = false): string | null {
32 if (offline) return null
33 const s = parseRpcOverride(raw)
34 return s !== null && isLoopback(s) ? s : null
35}
36
37/** Chain settings with an optional ?rpc= override: only that endpoint is used (WS on the same host and port, as
38 * anvil serves both). A local fork has no explorer and no Sourcify (its addresses and hashes are not on Sepolia). */
39export function chainWithRpc(base: ChainConfig, rpc: string | null): ChainConfig {
40 if (!rpc) return { ...base, ws: [...base.ws], http: [...base.http], historyHttp: [...base.historyHttp] }
41 const local = isLoopback(rpc)
42 return {
43 ...base,
44 ws: [rpc.replace(/^http/i, 'ws')],
45 http: [rpc],
46 historyHttp: [rpc],
47 explorerTx: local ? null : base.explorerTx,
48 explorerAddr: local ? null : base.explorerAddr,
49 sourcifyApi: local ? null : base.sourcifyApi,
50 pollMs: local ? 1000 : base.pollMs,
51 }
52}
53
54const RPC_OVERRIDE = rpcOverrideOf(params.get('rpc'), OFFLINE)
55
56/** Live view of a local Sepolia fork (?rpc= on a loopback address): numbers are labelled "local fork" */
57export const FORK = RPC_OVERRIDE !== null
58
59/** Offline mode has no endpoints and no outbound links at all, so nothing can open a connection by accident */
60export const CHAIN: ChainConfig = OFFLINE
61 ? { ...CONFIG.sepolia, ws: [], http: [], historyHttp: [], explorerTx: null, explorerAddr: null, sourcifyApi: null }
62 : chainWithRpc(CONFIG.sepolia, RPC_OVERRIDE)
63
64/** ?p=<id>: open on that proposal (a positive integer, else ignored) */
65export function parsePick(raw: string | null | undefined): string | null {
66 const s = (raw ?? '').trim()
67 return /^[1-9]\d{0,8}$/.test(s) ? s : null
68}
69export const PICK = parsePick(params.get('p'))
70
71/** ?clean=1 (or a bare ?clean): recording mode. The presenter chrome (language switch, Details, ?, key hints) is not
72 * drawn, so a recorded video shows only the product. ?clean=0 or no parameter: the normal page */
73export function parseClean(raw: string | null | undefined): boolean {
74 if (raw === null || raw === undefined) return false
75 const s = raw.trim().toLowerCase()
76 return s === '' || s === '1' || s === 'true' || s === 'yes'
77}
78export const CLEAN = parseClean(params.get('clean'))
79
80/** Guard cards in deployment order: guardians = [AirlockReceiver (Chainlink CRE), emergency guardian (human)] */
81export const GUARDS = ['cre', 'breakglass'] as const
82export type GuardId = (typeof GUARDS)[number]