Support parent-based Safe Apps on selected websites - #1616
KillariDev wants to merge 19 commits into
Conversation
|
/review |
There was a problem hiding this comment.
CI Agent Review
The Safe Apps hosting feature is gated behind multiple consent layers and the authorization model (compatibility mode + explicit origin + signing Safe + website access) holds up, but reviewers flagged several concerns. The most significant is the requestFinanceSafeDiscoveryAdapter.ts global window.setTimeout override applied to the default origin https://app.request.finance: it silently drops unrelated 200 ms timers scheduled during the synchronous discovery window and deliberately defeats the site's own fail-closed 200 ms discovery deadline, weakening a third-party site's security control on a live production page without per-origin consent. Additionally, a dead near-duplicate requestFinanceSafeHost.ts entrypoint is bundled and shipped but never loaded, and the page-injected serialized bridge code is co-located with background orchestration in prepareSafeApp.ts, creating a fragile hidden invariant that any future refactor could break silently.
|
/review |
There was a problem hiding this comment.
CI Agent Review
Multiple review agents identified issues in the new Safe Apps hosting feature. The most serious is in content-script registration: a single invalid Safe Apps host match pattern (e.g. an IPv6 origin such as https://[::1]:8443, which SafeAppsHostOrigins accepts but Chrome match patterns reject) aborts registration of the entire batch — including the base 'inpage'/'inpage2' provider — and the error is swallowed by a catch that only reports it, silently disabling provider injection on every site. Architecturally, the popup's 'Authorize and reload' flow reaches directly into background modules and privileged browser APIs (browser.tabs.query, browser.scripting.executeScript, browser.tabs.reload) instead of going through the popup↔background message protocol, duplicating website-access eligibility logic that the background already owns. The exported-settings schema adds safeAppsHostOrigins to the existing '1.6' variant without a version bump, which can reject previously exported v1.6 files on import. Finally, the set of settings that require content-script re-registration is now enumerated in two places that must be kept in lockstep.
|
/review |
There was a problem hiding this comment.
CI Agent Review
Two structural concerns from the architecture review. No security or correctness findings were confirmed by the other agents (their investigated-and-cleared items are intentionally omitted). (1) The background now embeds and serializes page-context code: prepareSafeApp.ts imports requestSafeAppConnection and passes it to browser.scripting.executeScript as func, splitting page-context code across app/inpage/ts and app/ts/utils/pageScripts, with only a comment guarding against module-import breakage; the Safe SDK wire protocol is also implemented independently in both safeAppsHost.ts and requestSafeAppConnection.ts and must be updated in lockstep. (2) contentScriptsUpdating.ts grows into a stateful background service living in utils/: a module-level singleton (updateContentScriptInjectionStrategyManifestV3) with mutable state and a lifetime browser.storage.onChanged listener, blurring the utils/background layering and forcing tests to construct fresh updaters around the shared instance.
|
/review |
1 similar comment
|
/review |
There was a problem hiding this comment.
CI Agent Review
Safe Apps hosting adds a MAIN-world shim that adapts parent-based Safe SDK messaging into the same-window provider, plus a Settings-driven connection preparation flow. Findings: (1) Security — embedded cross-origin frames in a hosted page can inject Safe Apps operations into the approved top-frame connection because the shim re-posts requests as self-posts, defeating the bridge's frame-origin boundary. (2) Unanswered getSafeInfo discovery requests permanently consume the shared 32-slot pending queue, wedging the page's Safe SDK after enough retries. (3) The Authorize-and-reload action holds the popup reply for up to five minutes, leaving the Settings UI stuck on a dismissed approval and producing untyped 'did not return a reply' failures if the tab closes mid-flight. (4) The Safe Apps wire protocol is duplicated in the inpage layer with already-diverged sdkVersion validation. (5) A wire codec now lives in utils/ while the types layer imports it, inverting the established types↔utils dependency direction.
|
/review |
There was a problem hiding this comment.
CI Agent Review
The Safe Apps hosting changeset has one functional bug and one architectural concern. The connection-preparation flow ('Authorize and reload open tab') injects a bootstrap script whose final expression returns a promise, but the injection omits awaitPromise: true, so in real Chrome the promise is never awaited and the action always fails with 'The website did not confirm a Safe connection.' Separately, the Safe Apps request-queue lifecycle policy (capacity limit, timeout, cancellation, overflow handling) is implemented twice — in the provider bridge and in the new host shim — so future protocol changes require coordinated edits in two modules.
|
/review |
There was a problem hiding this comment.
CI Agent Review
Architectural fragility in the new Safe Apps hosting subsystem: (1) 'Authorize and reload' can reload a tab before the safe-apps-host content script is actually registered, because the prepare flow reads persisted settings but never awaits/verifies the asynchronous registration service; (2) the MV2 document-start embedding step now independently bundles the provider and writes its IIFE back over app/inpage/js/inpage.js, which the main bundler also produces — two build tools own the same artifact with unenforced ordering; (3) origin-to-Chrome-match-pattern translation is duplicated (getManifestV3ExcludeMatchesForOrigin vs getSafeAppsHostMatchPatterns) with divergent rules and merged into a single excludeMatches array, requiring future pattern fixes to be applied in lockstep.
|
/review |
1 similar comment
|
/review |
There was a problem hiding this comment.
CI Agent Review
One guaranteed test failure: getChromeMatchPatterns retains an explicit default port in site-with-subdomains scope (https://*.example.com:443/*) while the new test expects it dropped (https://*.example.com/*). The Safe Apps host shim also introduces irreversible page mutations (window.parent redefinition plus a hidden iframe) with no lifecycle short of a full page reload, and its relay depends on an implicit provider window.postMessage contract that would break silently if the provider's response channel changes. The preparation flow duplicates the Chrome-only executeScript workaround in two places.
|
/review |
There was a problem hiding this comment.
CI Agent Review
The Safe Apps hosting changeset is extensive and well-tested, but it extends the versioned settings import/export ladder for a fifth time ('1.7') and spreads the lockstep coordination burden into the new content-script registration service, which now maintains a parallel, manually-synced triage of the same settings (its own storage-key list, host-match/exclusion derivation, and a recovery outcome interpreted by callers). Future settings versions will require coordinated edits across exportedSettingsTypes.ts, background/settings.ts, and background/contentScriptRegistration.ts.
|
/review |
There was a problem hiding this comment.
CI Agent Review
Three concerns in this changeset. (1) getSafeAppsHostOrigins reads persisted safeAppsHostOrigins through the throwing SafeAppsHostOrigins funtypes codec; malformed stored values (wildcards, duplicates, >32 entries) are treated as an expected failure by the content-script recovery path (the Chrome benchmark deliberately corrupts the key), yet settingsOpened(), exportSettingsAndAddressBook(), and prepareSafeAppTab() read it directly and crash instead of falling back to DEFAULT_SAFE_APPS_HOST_ORIGINS. (2) Safe Apps preparation cancellation races with the in-flight MAIN-world prepare bootstrap: cancelSafeAppPreparation injects cancelSafeAppPreparationBootstrap.js into the same document while the prepare script is still installing its SAFE_APPS_PREPARATION_CANCEL_EVENT listener, and Chrome does not guarantee ordering between the two executeScript calls; the orphaned probe can then post a genuine getSafeInfo request that triggers requestAccessForDiscovery() and an interactive website-access prompt after the user cancelled, while the UI already reports success. (3) contentScriptRegistration.ts fuses the extension-wide base provider registration and the feature-specific Safe Apps host registration into one reconciliation transaction and failure-recovery state machine, so Safe Apps host failures are reported as base-provider recovery, Safe Apps settings changes recompute base provider exclusions, and disableInterceptorForPage gates the core enable/disable flow on the Safe Apps registry's health.
|
Addressed the three points from this review in 5cde2ea:
Added focused regressions, including a delayed injection race and an A → failed B → A registration retry. Updated the Chrome communication harness for the host-specific error code. Validation passed: |
|
/review |
There was a problem hiding this comment.
CI Agent Review
The Safe Apps hosting feature introduces maintainability and consistency concerns. The injected-script inventory is now a string-literal contract spread across the bundler, the background injection calls, and the manifest tests, with load-bearing execution order encoded only in arrays (adding a script requires coordinated edits and a mismatch fails at runtime, not build time). The content-script registration service hides a retry/outcome protocol: update() discards its result, so a cached 'hosting-failed' outcome is never retried until the settings key changes, and only ensureSafeAppsHostRegistered can heal it via an undocumented generation counter. Domain limits (32 origins) and the user-facing cancellation message are duplicated across layers and can drift. The host also silently drops malformed Safe Apps requests instead of returning an error response like the provider does.
| path.join(appDirectory, 'inpage', 'js', 'inpage.js'), | ||
| path.join(appDirectory, 'inpage', 'js', 'listenContentScript.js'), | ||
| path.join(appDirectory, 'inpage', 'js', 'listenContentScriptBootstrap.js'), | ||
| path.join(appDirectory, 'inpage', 'js', 'safeAppsHostBootstrap.js'), |
There was a problem hiding this comment.
The five new injected page scripts (safeAppsHostBootstrap, prepareSafeAppBootstrap, cancelSafeAppPreparationBootstrap, clearSafeAppPreparationCancellationBootstrap, readDocumentOrigin) form a string-literal contract spread across this inventory, the injection calls in prepareSafeApp.ts (lines 25/31/104/114), the host script's js array in contentScriptRegistration.ts (line 73), and the MV2 web_accessible_resources assertions in tests. The IIFE-vs-ESM decision is encoded as a positional equality check (entrypoint === options.inpagePath) and the host-bootstrap-before-provider ordering is just an array. Adding one injected script now requires coordinated, compile-time-invisible edits in at least three files, and a mismatch fails at runtime rather than at build time. Consider a single shared inventory/constants module and typed result contracts for the injected scripts.
| return nextUpdate | ||
| } | ||
| // Ordinary reloads only need readiness; hosting-specific success and retry decisions remain inside the service. | ||
| const update = async () => { await queueUpdate() } |
There was a problem hiding this comment.
update() awaits queueUpdate() but discards its result, so after a hosting failure the cached appliedOutcome === 'hosting-failed' persists and update() never retries until the settings key changes; the failure is only reported out-of-band via reportUnexpectedError. The only retry path is ensureSafeAppsHostRegistered, which requires the caller to pass an internal generation counter (retryRecoveredAttempt === appliedAttempt) and then re-query getRegisteredContentScripts() itself to determine success. The returned 'applied' therefore does not mean registrations are in place. This hidden mutable-state protocol is undocumented and easy for future callers to misuse; consider returning the outcome from update() and encapsulating the retry/verification inside the service.
| try { return parseSafeAppsHostOrigin(value) === value } catch { return false } | ||
| }) | ||
|
|
||
| export const SafeAppsHostOrigins = funtypes.ReadonlyArray(SafeAppsHostOrigin).withConstraint((origins) => origins.length <= 32 && new Set(origins).size === origins.length) |
There was a problem hiding this comment.
The 32-origin cap is enforced here (origins.length <= 32) and independently re-encoded in SafeAppsHostingSettings.tsx line 30 (origins.length >= 32). Changing the limit requires editing both files with nothing tying them together, and the UI can disable 'Add website' at a different count than the validator accepts. Export a shared constant for the limit from this module.
| if (operation === undefined) return | ||
| // Keep the operation reserved until page cleanup finishes; a late cancellation must not cancel a subsequent retry. | ||
| operation.cleanup ??= clearPreparationScript(operation) | ||
| operation.abort.abort('Safe connection was cancelled.') |
There was a problem hiding this comment.
The user-facing cancellation string 'Safe connection was cancelled.' is produced in three places that must agree: requestSafeAppConnection.ts (the marker check and the cancel-event handler), this abort.reason, and the cancelledReply fallback (line 63). If these diverge, the popup and the page report different outcomes for the same cancellation. Centralize the message in the shared protocol module.
| const onRequest = (event: MessageEvent<unknown>) => { | ||
| if (disposed || event.source !== windowObject || event.origin !== windowObject.location.origin) return | ||
| const message = event.data | ||
| if (!isSafeAppsRequest(message)) return |
There was a problem hiding this comment.
Unlike the provider, which replies with an error when a request has an invalid env.sdkVersion or a missing method, the host's onRequest silently drops such messages (isSafeAppsRequest returns false). This only affects malformed requests the Safe Apps SDK never produces, so it is low severity, but the two bridges now behave inconsistently; consider returning an error response for symmetry.
|
Addressed this review in be88c35, and merged current
Validation passed after these changes: |
|
/review |
There was a problem hiding this comment.
CI Agent Review
Three concerns surfaced in review: (1) the Settings 'Add website' action is not gated on the Safe Apps compatibility toggle, so users can accumulate persisted host origins while the feature is disabled and those origins become live hosting targets the moment the toggle is re-enabled; (2) the settings module now computes Chrome content-script match patterns, creating two sources of truth for origin-to-pattern generation alongside contentScriptRegistration.ts that can silently diverge; (3) the popup request/reply channel now carries a long-running, tab-mutating automation workflow whose cancellation semantics span background and page code across several files.
| <button type = 'button' class = 'button' disabled = { action.value.state === 'pending' } onClick = { () => waitFor(async () => await saveOrigins(origins.filter((existing) => existing !== origin))) }>Remove</button> | ||
| </div>) } | ||
| <label>Website URL <input type = 'url' value = { website.value } placeholder = 'https://app.example.com' onInput = { (event) => { website.value = event.currentTarget.value } } /></label> | ||
| <AsyncActionButton state = { action.value.state } disabled = { website.value.trim() === '' || origins.length >= SAFE_APPS_HOST_ORIGIN_LIMIT } text = 'Add website' pendingText = 'Saving…' class = 'button' onClick = { () => waitFor(async () => { |
There was a problem hiding this comment.
The 'Add website' action is not gated on the enabled (Safe Apps compatibility) toggle, unlike the per-origin 'Authorize and reload open tab' button which is disabled with !enabled. While compatibility is off, the Add row remains fully usable and persists new entries to safeAppsHostOrigins via popup_ChangeSettings; those entries are inert while disabled but become live hosting targets the instant the toggle is re-enabled, without further confirmation, and they also round-trip through settings import/export. Gate this button (and saveOrigins) on enabled to match the Authorize button's behavior.
| } | ||
|
|
||
| // Read once for both the cache identity and desired registrations; validate hosting separately so its corruption cannot disable the ordinary provider. | ||
| export async function getContentScriptConfiguration(): Promise<ContentScriptConfiguration> { |
There was a problem hiding this comment.
The settings module now owns content-script injection configuration: getContentScriptConfiguration()/getHostingConfiguration() compute Chrome browser.scripting match patterns (excludeMatches, hosting matches with default-port handling), and contentScriptRegistrationSettingsKeys is re-exported so consumers treat settings as the owner of scripting configuration. Structurally this overloads the settings boundary with browser.scripting internals, and the origin-to-pattern mapping is derived in two places (here for the host's matches, and again inside contentScriptRegistration.ensureSafeAppsHostRegistered()), creating two sources of truth that can silently diverge on match-pattern or default-port changes. Consider keeping registration config derivation inside contentScriptRegistration.ts and leaving settings.ts to answer only about user settings.
| import { popupMessageHandler, popupSnapshotMessageHandler, type PopupMessageHandlerMap } from '../popupMessageHandlerRegistry.js' | ||
|
|
||
| export const safePopupMessageHandlers = { | ||
| popup_prepareSafeApp: popupMessageHandler('popup_prepareSafeApp', async (_context, request) => ({ method: 'popup_prepareSafeApp', data: await prepareSafeAppTab(request.data.origin) })), |
There was a problem hiding this comment.
The popup request/reply channel now hosts a long-running, tab-mutating automation workflow: popup_prepareSafeApp blocks its background handler for up to 30 seconds (or indefinitely until cancelled) while orchestrating isolated-world document-ID reads, MAIN-world page-script injection into a third-party tab, page-side promise settlement, host-registration waits, and finally browser.tabs.reload(). Cancellation semantics span a four-file choreography (AbortController + cleanup promise, cancelSafeAppPreparationBootstrap marker/event, waiting for the prepare script, then clearing the marker). This grows the popup RPC surface with implementation details (document IDs, cancellation markers, reload timing) that callers must understand. Consider giving the hosting workflow a dedicated abstraction with its own lifetime boundaries rather than extending the origin-keyed state machine through the popup protocol.
|
Addressed this review in 98201f9:
Validation passed: |
|
/review |
There was a problem hiding this comment.
CI Agent Review
Two bugs were reported in the new Safe Apps hosting code: (1) an unhandled promise rejection when a tab is closed during connection preparation, since the losing pageScript promise is never observed, and (2) shared async state in the Settings component makes unrelated buttons show misleading pending labels and become disabled while any single action is running.
| operation.documentId = document.documentId | ||
| const pageScript = injectFiles({ target: { tabId: tab.id, documentIds: [document.documentId] }, world: 'MAIN', files: [INPAGE_SCRIPTS.prepareSafeApp] }) | ||
| operation.pageScript = pageScript | ||
| const results = await Promise.race([pageScript, operation.cancelled]) |
There was a problem hiding this comment.
Unhandled promise rejection: when the tab is closed during preparation, browser.tabs.onRemoved aborts the operation and Promise.race settles on operation.cancelled, but the losing pageScript promise (chrome.scripting.executeScript targeting the removed document) later rejects with 'The tab was closed.' or 'No document with id...'. Nothing observes this rejection: clearPreparationScript only catches it on the explicit cancel path, and start()'s catch only sees the rejection if pageScript rejects before operation.cancelled resolves. The rest of the background code consistently attaches rejection sinks (silenceChromeUnCaughtPromise/.catch); attach one to pageScript after the race (e.g. pageScript.catch(() => undefined)) so the losing promise's rejection is always observed.
| const website = useSignal('') | ||
| const connectingOrigin = useSignal<string | undefined>(undefined) | ||
| const { value: cancellation, waitFor: waitForCancellation } = useAsyncState<void>() | ||
| const { value: action, waitFor } = useAsyncState<void>() |
There was a problem hiding this comment.
All action buttons share the same 'action' useAsyncState signal, so while any one action is pending every AsyncActionButton bound to it renders its own pendingText (e.g. 'Connecting…'/'Saving…') and is disabled for unrelated rows. Clicking 'Authorize and reload open tab' (which can stay pending during the access popup) disables 'Add website' and 'Remove' and shows misleading spinner text on every row, and vice versa while saving a website. Use separate useAsyncState instances per action (one for website-editing saves, one for preparation) or key pending state per button/origin.
|
Addressed review 5401226550 in 7c8c12b.
Validation: |
|
/review |
There was a problem hiding this comment.
CI Agent Review
Five issues surfaced across the review agents for the Safe Apps hosting changeset. (1) contentScriptRegistration.ts can return a cached 'configuration-applied' outcome while persisted dynamic registrations have been removed, so ensureSafeAppsHostRegistered fails spuriously and the 'Authorize and reload' flow reports a hosting error despite correct settings. (2) The in-page provider queue only expires abandoned discovery on the next message, so a stale getSafeInfo probe is re-issued when approval arrives after the site gave up, producing a delayed round-trip and a stale 'Safe connection timed out' error in the preparation flow. (3) Importing any pre-1.7 settings backup unconditionally writes safeAppsHostOrigins back to an empty list, silently destroying an existing user's separately-maintained hosting selection. (4) The safe-apps-host overlay adds a second MAIN-world provider injection path that must be hand-synchronized with the base registration, including a hardcoded 'safe-apps-host' ID exemption in reconcileBaseContentScripts. (5) The Safe SDK response version is pinned in package.json, hard-coded in safeAppsProtocol.ts, and contradicted by @web3-onboard/gnosis's ^8.0.0 peer range, leaving the protocol version subject to silent drift.
| const settingsKey = configuration.cacheKey | ||
| // Concurrent callers may retry the failure they observed once; a newer queued attempt owns subsequent retries. | ||
| const retryObservedFailure = appliedOutcome === 'hosting-failed' && failedAttemptToRetry === appliedAttempt | ||
| if (settingsKey === appliedSettingsKey && !retryObservedFailure) return appliedOutcome |
There was a problem hiding this comment.
Stale outcome cache: update() returns a cached 'configuration-applied' result whenever settingsKey === appliedSettingsKey, but dynamic content-script registrations persist across service-worker restarts and can be removed by a competing or previously applied configuration. ensureSafeAppsHostRegistered then re-checks getRegisteredContentScripts() and can return false despite correct settings, so the 'Authorize and reload' flow fails with 'Safe Apps hosting could not be registered for this website. Check the hosting settings and retry.' until the user toggles settings or retries. Treat the cached outcome as valid only when the actual registered scripts still match, or reset the cache on worker start.
| setEnabled(nextEnabled: boolean, nextCanRequestAccess = false) { | ||
| pendingRequests.expire() | ||
| canRequestAccess = nextCanRequestAccess | ||
| if (enabled !== nextEnabled) enablementGeneration += 1 |
There was a problem hiding this comment.
Abandoned discovery probes are only expired lazily. The provider queue has no timers, so pendingRequests.expire() runs only on incoming messages and setEnabled. A page that gives up on a getSafeInfo probe (e.g., after its own 200 ms discovery deadline) leaves the entry queued past SAFE_APPS_REQUEST_TIMEOUT_MS; when approval arrives, setEnabled(true) drains the queue and answerRequest re-issues the stale request to the background instead of discarding it. In the preparation flow this produces a delayed, meaningless round-trip and a stale 'Safe connection timed out' error even when approval succeeds; on re-enablement of another guest it can trigger an unnecessary access prompt. Clean up abandoned discovery on its own deadline rather than relying on the next message.
| safeAppsCompatibilityMode: 'safeAppsCompatibilityMode' in settings ? settings.safeAppsCompatibilityMode : false, | ||
| safeAppsHostOrigins: 'safeAppsHostOrigins' in settings ? settings.safeAppsHostOrigins : DEFAULT_SAFE_APPS_HOST_ORIGINS, | ||
| } | ||
| } |
There was a problem hiding this comment.
Legacy import silently wipes the hosting list. normalizeImportedSettings yields DEFAULT_SAFE_APPS_HOST_ORIGINS ([]) for every export predating 1.7, and importSettingsAndAddressBook calls setSafeAppsHostOrigins(settings.safeAppsHostOrigins) unconditionally. A user who has configured hosted websites and imports an older backup will have that separately-maintained selection destroyed even though the backup says nothing about hosting. Only write hosting when the export actually carries safeAppsHostOrigins.
| const desiredContentScriptIds = new Set(contentScripts.map(({ id }) => id)) | ||
| const missingContentScripts = contentScripts.filter(({ id }) => !registeredContentScriptIds.has(id)) | ||
| const existingContentScripts = contentScripts.filter(({ id }) => registeredContentScriptIds.has(id)) | ||
| const obsoleteContentScriptIds = registeredContentScripts.map(({ id }) => id).filter((id) => id !== 'safe-apps-host' && !desiredContentScriptIds.has(id)) |
There was a problem hiding this comment.
The safe-apps-host overlay creates a second, coordinated MAIN-world provider injection path that must be hand-synchronized with the base registration. reconcileBaseContentScripts hardcodes an exemption for the overlay's script ID (id !== 'safe-apps-host'), reconcileSafeAppsHost re-derives and updates the base inpage script with combined exclusions, and config/injectedScripts.ts re-lists the provider entrypoint in SAFE_APPS_HOST_SCRIPTS. Any future change to the provider's injected file set — or a second overlay — must be applied in multiple files or hosted pages either lose provider injection or end up with duplicate MAIN-world providers. Compose the overlay from a single shared provider-script definition and a registration list the base routine can derive without built-in ID exemptions.
| import safeAppsPreparationMessages from '../../shared/safeAppsPreparationMessages.json' | ||
|
|
||
| // Canonical SDK envelope used by the provider, parent host and Settings preparation. | ||
| export const SAFE_APPS_RESPONSE_VERSION = '9.1.0' |
There was a problem hiding this comment.
The effective Safe Apps protocol version is hand-pinned in three places that are not derived from one another: package.json (@safe-global/safe-apps-sdk@9.1.0), this constant, and the new @web3-onboard/gnosis@2.3.2 peer range (^8.0.0), which contradicts the pinned version and vendors its own transitive viem@2.12.0 beside the root viem@2.57.1. Every SDK upgrade requires lockstep edits across all three (plus the semver validator in parseSafeAppsRequest), and a lockfile refresh can resolve web3-onboard's peer differently since the declared range cannot be satisfied by the pinned version. Derive the version from a single source or align the peer constraint.
Safe Apps that use parent-window discovery can connect through Interceptor on explicitly selected websites. Settings can authorize and reload an open app after the user selects a Safe in signing mode.
Changes
awaitPromiseoption, as documented by the scripting API.Validation
bun run test: 1,544 passed, zero failures.bun run setup-chrome,bun run typecheck,bun run lint: passed separately in required order after tests.bun run test:chrome-safe-apps-host: passed with real SDK/Wagmi, HTTP/HTTPS, unapproved cross-origin iframe requests, Settings preparation and cancellation during approval, including the realchrome.scripting.executeScriptfile promise result.bun run setup-firefox: passed; the final generated bundle was restored to Chrome before browser checks.bun run test:chrome-request-finance-discovery: passed; the original 200 ms deadline fires during an 11-second access approval, and late SDK replies preserve that outcome. Settings preparation/reload receives the configured Safe without another signer prompt.bun run test:chrome-communication: passed, including real Chrome IPv6 host registration, corrupt stored configuration recovery, recorded registration diagnostics, and ordinary access approval.bun run test:chrome-safe-cosigning-communication --safe-apps-only: passed, including reloads with stalled signer chain queries and no repeated signer prompt.Project reviewer: 95/100; no High, Medium or Low findings. Review 5394174332: centralized backup schemas/import normalization and content-script configuration ownership. Regressions exercise every historical format, preserve disabled malformed hosting data as inert, and verify a settings change after snapshot capture is applied by the subsequent update rather than mixing cache identity and registration inputs.
Compatibility scope
Hosting targets Chrome MV3 and parent-based Safe SDK discovery. Authorization and reload remove the interactive approval delay; they do not guarantee responses within a site's deadline. Request Finance's 200 ms discovery can still time out during extension initialization after approval. Apps with short deadlines need connector configuration or discovery changes in the app; the real Wagmi browser fixture uses its documented 5-second timeout option.
Apps requiring a real iframe or trusted Safe parent origin, including Web3-Onboard's strict iframe detection, need a separate launcher or integration. Browser checks use Chrome 145, local fixtures, a configured test Safe, and fake signer/RPC services. Live Request Finance, real wallets, other Chrome versions, and Firefox hosting were not exercised. Generated build outputs are not committed.