Skip to content

Release History

magicelk235 edited this page Aug 23, 2026 · 29 revisions

Release History

viaduct is versioned with npm semver and shipped from git tags. Every published GitHub Release triggers a GitHub Action that publishes the package to npm as @magicelk235/viaduct over Trusted Publishing (OIDC): no long-lived NPM_TOKEN. The current released version is 1.12.0. The project spans roughly 185 commits from 2026-05-29 (scaffold) to 2026-07-22 (working branch), and this page maps each tag to what actually shipped in it, grounded in the commit record.

Every bullet cites the (shorthash) it came from. Newest release is on top.

Cross-references: Runtime Shim · Manifest Transform · Analyzer · Build and Install · OAuth Bridge · Safari Quirks


v1.12.0, 2026-08-23

Theme: debuggable conversions, and the two Safari behaviors between uBlock Origin and a usable install.

  • Add --debug conversions and a --logs reader (a10cbdf). A debug conversion flips the shim's compiled-in trace gate at staging time and splices in a ring buffer persisting the last 2000 entries to storage.local under __viaduct_debug_log__; Safari keeps that store as SQLite on disk, so viaduct --logs <name> reads it back with Safari open. Release conversions carry no trace of the machinery. See CLI Reference and Testing and Debugging.
  • Correct the sender on ports, where Safari's sender slot is a read-only exotic getter (59d65b7). The message path hands its corrected sender clone to listeners as an argument; onConnect delivers the Port itself, so the old port.sender = fixed assignment silently bounced off and connect-time privilege gates saw the raw value — uBlock's popup port never passed origin === getURL('').slice(0,-1) and the popup rendered blank. The wrapper installs the corrected sender in place where the slot allows it and otherwise substitutes a memoized delegating Port differing in sender alone; non-own-origin senders pass through native. See Safari Quirks B4.
  • Normalize webRequest frame types, anchored on the event prototype (64a8ab9). Safari labels every frame's document load main_frame with a raw 64-bit frameId, so uBlock's strict blocker replaced the tab with its interstitial over ad-iframe cookie syncs on every w3schools visit; the Chrome-impossible shape now reaches listeners as sub_frame. The patch lives on the shared event prototype because WebKit holds event wrapper objects weakly and hands later property accesses fresh, expando-free wrappers — an instance patch (even pinned) covered boot-time registrations and missed uBlock's post-boot onResponseStarted. See Safari Quirks D4a.
  • The analyzer stops claiming userScripts registrations are never injected (ef96792), untrue since the 1.9.0 emulation; the copy now names the real gaps (document_start timing, the USER_SCRIPT-world CSP exemption). A conversion-regression workflow (9e92dd1) runs typecheck, the suite, and an --analyze --json gate over synthesized MV2/MV3 fixtures on every push. The README was restructured and documents --background-launch, --config, --list, batch mode, and --strict with --analyze (addad06).

Verified in Safari 26 with real conversions: uBlock Origin's popup renders its counters, six consecutive w3schools loads finish on w3schools with forty ad cookie-sync frames observed arriving normalized, and the strict-block interstitial still appears for a genuine top-level navigation to a listed tracker domain.

v1.11.9, 2026-08-22

Theme: one extension that had stopped working in Safari, and the three separate reasons why.

  • Never replace the global chrome/browser (895e7a3). WebKit does not hand a message to the listener you registered: on every dispatch it re-reads the frame's global browser, then chrome, and casts what it finds, and the cast only succeeds on Safari's own namespace object. The webRequest filter sanitizer used to republish both globals as Object.create(native) roots whenever Safari refused an in-place swap of the webRequest slot, and from that moment Safari skipped the context: listeners stayed registered, senders resolved undefined with no lastError, nothing was delivered. Live, that was TWP's content script asking the background to translate, so no page ever translated. The sanitizer now patches addListener on the event instance, or on the shared wrapper prototype when the instance is frozen, and leaves the globals alone; if neither patch takes it leaves the event native, because one bad filter throwing beats every inbound message being dropped. See Safari Quirks E15.
  • Accept the menu contexts and file patterns Safari rejects (cf96d63). menus.create throws on Chrome's MV2 context aliases ('page_action' is not a valid context) and on host-wildcard file patterns ('file://*/*' is not a valid pattern), and each throw silently drops the item. page_action/browser_action now map to action as current WebKit does, Chrome OS-only launcher items are skipped, and file://*/* is rewritten to the hostless form Safari's parser accepts.
  • Stop the light floor from beating an extension's own dark mode (b42c3cf). The injected color-scheme floor painted html as well as body, and an extension that picks its theme in JavaScript rather than CSS looks light-only to the detector. A rule like TWP's html *{background-color:#181a1b!important} can never match the root, so the canvas stayed white behind the app's dark UI: the options page rendered as a dark centre column with white margins, the popup as a dark strip on white. The floor now paints body only — a transparent html propagates the body background to the canvas, so a real light page still gets Chrome's white page and the extension's own rule wins as soon as it lands. Root-absolute stylesheet hrefs (/css/app.css) are also resolved from the extension root now, instead of being joined onto the page's directory and going unread. See Safari Quirks F2a.

Verified in Safari 26 with a real conversion: TWP translates a Spanish Wikipedia article end to end, its action menu items are present, its options page and popup are uniformly dark with the OS in dark mode, and "Chrome extension source viewer" still renders black on white.

v1.11.8, 2026-08-22

Theme: installing without the host app taking the screen.

  • Add --background-launch (c74bd2b). Installing has always finished with a plain open on the built host app, because launching it is what makes PlugInKit register the extension with Safari, and the side effect was the app's window landing on top of whatever you were looking at. With the flag the host app launches through open -g -j instead: registration is unchanged, nothing appears on screen, and the caller decides when to show the app. The unsigned path's Safari relaunch gets the same -g treatment. Nothing changes unless the flag is passed. See CLI Reference.

v1.11.7, 2026-08-21

Theme: a login that never survived quitting Safari, because the auth tab's redirect was invisible and the session it recovered died with the background page.

  • See the redirect Safari never reports. A silent launchWebAuthFlow opened its tab and then attached its own webNavigation listeners, which is the shape that works in Chrome. Safari delivers nothing to a listener the background page added after it finished evaluating, so those listeners heard no event at all: every silent re-auth ended 8s later with "tab never navigated" and an empty navigation list, while Safari's own history showed the authorize page had loaded and redirected fine. Measured with the page provably alive, a 2s heartbeat ticking right through the attempt. The observers are now registered once at load and routed to whichever flow owns the tab, fed by webNavigation, tabs.onUpdated, and a 250ms poll of tabs.get — the poll being the source that actually reports the redirect on Safari 18, which turns an 8s failure into a ~2.5s success. Timeouts route through the same exit as every other outcome, and each attempt appends one redacted entry to __c2sAuthLog, so an attempt that quietly ran out of time no longer leaves nothing behind.
  • Keep storage.session across a background restart. Chrome keeps it for the life of the browser session, which is why an extension is willing to keep live state there at all; Claude for Chrome migrates its OAuth tokens into it on purpose, to keep them out of a file. Safari hands every page load its own store, so a converted background came back empty seconds after going idle: measured, one page stored a valid token and the next read none, so every wake answered the panel with a login screen and opened another re-auth tab. The owner now mirrors its session store into storage.local and restores it on the next background page, holding reads until the restore lands. The mirror is stamped and expires after 12 hours, bounding how long values kept off disk are on it; runtime.onStartup never fires for a converted background, so a deadline is the only signal there is.
  • Two dead ends worth recording, both measured: Safari ignores state:"minimized" on windows.create and clamps an off-screen popup back onto the display, so there is no invisible surface to run a silent flow in. A background tab stays, and the mirror is what keeps it rare.

Verified on Claude for Chrome 1.0.85: quit Safari, reopen, and the panel comes up signed in with no auth tab opened at all.

See Safari Quirks E13a, E13b and E14a.

v1.11.6, 2026-08-21

Theme: a silent token refresh that was being timed out before the provider had been asked.

  • Spend the caller's non-interactive deadline on the provider, not on our tab setup. launchWebAuthFlow({interactive:false}) measures how long the provider takes, which in Chrome is all it pays for. Here it was also paying for a tab being created and Safari loading the authorize page cold, so Claude's 5s budget was routinely gone before claude.ai had been reached: the redirect landed after the attempt was abandoned, the caller fell back to an interactive login, and the user got a tab flashing past and a login screen for no reason. Setup now has its own 8s allowance, the caller's window starts when the auth page first navigates, and a 20s ceiling bounds the attempt because callers race timers of their own.

See Safari Quirks E13.

v1.11.5, 2026-08-21

Theme: the other half of staying signed in. A token the background holds has to be readable by the panel that asks for it.

  • Give the extension one storage.session. Chrome keeps it in the browser, shared by every context and outliving a worker restart; Safari gives each context its own space, so the ordinary pattern of state the background owns and a panel reads comes back empty with nothing to explain it. Claude for Chrome asks the background whether its token is valid and then reads the token itself, so a valid answer was followed by an empty read and a login screen. The background page stays the owner and every other extension page forwards to it over the relay, which buys Chrome sharing semantics without persisting anything the extension kept out of storage.local. An unreachable owner falls back to the local space, so it is never worse than before.
  • Two guards came out of building it. The owner registers after the storage relay installs, since the relay mirrors mailbox records only into listeners it recorded itself. And the proxy is skipped when storage.session is the same object as storage.local, because patching an aliased object sends the first read over the mailbox into itself until the stack runs out; that one was caught by an existing test rather than by a user.

See Safari Quirks E14.

v1.11.4, 2026-08-20

Theme: a build that failed and would not say why, reported against Yomitan (#15).

  • Print the diagnostics xcodebuild actually wrote. It splits a failure across both streams: the error: lines that name the cause go to stdout, while stderr carries run-destination noise and ** BUILD FAILED **. The build printed stderr and only fell back to stdout when stderr was empty, which it never is, so every failed build reported a summary with no cause, in the terminal and in the bug report alike. The error: lines from both streams are now collected, de-duplicated across targets (each signing failure is repeated once per target), and capped.
  • Stop --team auto from picking a team the Mac cannot sign for. A team id read off a provisioning profile was treated as proof of an account, and profiles outlive the account that installed them, so on a machine with no Apple account the reporter's run detected a third-party vendor's team and every build died on error: No Account for Team "…". Only Xcode's account cache and the keychain nominate a team now, since those mean an account or a usable certificate exists; the profiles still choose between them, newest first, so the team you most recently provisioned for keeps winning when the keychain holds several.
  • Retry ad-hoc when a detected team fails the build for a signing reason (expired certificate, revoked account, a free team out of app ids). --team auto means "team-sign if you can", so the conversion now finishes with an ad-hoc app and says so, instead of throwing the whole run away over signing. The rest of the run follows the signature that actually exists: the install skips the team-signed path, the unsigned-toggle warning prints, and the signature check expects ad-hoc. A team named with --team <id> is a deliberate request and still fails loudly rather than being downgraded.
  • Say what the archive held when there is no manifest.json. The error named the temp directory it had unpacked into and nothing else, which is unactionable in a report; it now lists the top-level entries and points at the folder or .crx/.zip to use instead.

See Build and Install for the detection order and the fallback, and test/xcodebuild-signing-failure.test.js / test/detect-xcode-team.test.js for the regressions.

v1.11.3, 2026-08-19

Theme: staying signed in. Two Safari behaviors between a converted extension and a token that refreshes itself.

  • Answer the question a bundle asks about whether it IS the service worker. The conversion makes the background a page, so ServiceWorkerGlobalScope is gone and code that routes on it takes the branch meant for panels: it messages the background, which is itself, and nothing answers. Claude for Chrome routes its OAuth refresh and its startup re-auth that way, so its tokens (kept in storage.session, which does not survive the background page being torn down) stopped being refreshed and the user was sent back to a login screen after a while. Answered at runtime in viaduct-sw-lifecycle.js, which only the generated background page loads, so panels still say no and still ask the background rather than racing it over a single-use refresh token. A build-time rewrite could not do this: the module that asks is shared with the panel.
  • Honor launchWebAuthFlow({interactive:false}). It is a silent refresh, and Safari has no silent surface, so the polyfill drives a tab. Driven with a focused tab on the interactive 120s ceiling, every refresh stole focus and left a stray tab on a login screen long after the caller had given up. A non-interactive call now gets a background tab and the caller's own deadline, and abortOnLoadForNonInteractive ends the attempt as soon as the auth page loads anywhere other than the redirect target.

See Safari Quirks E12 and E13.

v1.11.2, 2026-08-19

Theme: the one SPA navigation a converted extension never heard about, reported by a Cloaked user whose login said it had worked.

  • Watch location.href continuously in a content script where the extension actually reads webNavigation.onHistoryStateUpdated. Sampling only in bursts after user input misses a route change the page performs on its own, and measured on Safari 26 a page-world history.pushState also re-injects no content script, so that navigation produced no report and no event at all. Cloaked installs its page↔extension bridge from this event and its dashboard pushes the extension-auth status route once the token exchange returns, seconds after the last click: the bridge was never installed, the dashboard's EXTENSION_AUTH_COMPLETE had nobody to hand the session to, and the user was left staring at a Log In screen behind a page that said "Extension Authentication Successful". The watch is armed by the background's answer to a report, which says whether anything is listening, so it rides a message that already exists and an extension that never registered a listener still runs no timer. The full round trip is measured on Safari 26 with a purpose-built probe extension: report in ~130 ms, event with the right tabId and frameId: 0, and the background's tabs.sendMessage back to that tab received and answered. See Safari Quirks C5, Tested Extensions.
  • The analyzer stops advising what the shim already does. webNavigation.onHistoryStateUpdated was reported as a warning whose fix read "monitor history changes from a content script", which is exactly the emulation in place since 1.9.x; it is now an info [shimmed] finding that names the real remaining limits (a hidden tab, a subframe, an ungranted site).

v1.11.0 and v1.11.1, 2026-08-19

Theme: the page-to-background bridge, which had four separate ways to fail in silence, all of them found by driving Claude for Chrome's login.

Every fix here is in the bridge templates or the manifest, and every one applies to any extension with an externally_connectable flow rather than to Claude specifically.

  • Give the page world its bridge even when Safari skips the world:"MAIN" entry. Safari honors that key from 18.4 and ignores it in silence below, so the page had no chrome.runtime, messaged nobody, and left its login button spinning while the relay, the background and the polyfill all reported healthy. applyOAuthBridge has made page-bridge.js web-accessible for a script-tag fallback since the first bridge commit and the fallback was never written; the relay now probes the page world and injects the file itself when nothing answers. A page CSP can still refuse it, which is what world:"MAIN" was for, and that case says so.

  • Raise strict_min_version to 18.4 for world:"MAIN" entries the conversion injected and that cannot recover on their own, instead of shipping a manifest that claims 15.4 and quietly does less. Entries the extension declared itself keep the author's floor, and the page bridge is exempt because it re-injects itself.

  • Probe both transports into the background. A sendMessage into a suspended background is not reliably delivered or rejected, so a single relayed send could vanish with no error anywhere. The relay pings three times over sendMessage, then three times over the shim's chrome.storage.local mailbox, and sends the payload once over whichever answered. The ping is answered by the polyfill rather than forwarded to the extension, so it works for a bundle that has never heard of a ping and stays safe to repeat.

  • Take the bridged sender's origin from the page URL rather than Safari's sender.origin. The relay hands over a content-script sender whose origin is Safari's choice and not reliably the page's, and an allow-list handed the wrong origin refuses the message while holding the channel open: nothing throws, nothing logs, the page waits out its timeout.

  • Give the bridged sender a tab. Chrome always supplies one for an external page message and handlers act on it, which is how the consent window dismisses itself. Neither transport can produce a tab, so the polyfill resolves it from the page URL, refuses a candidate that is visibly another site, and gives up after 1.5s rather than becoming a new way to hang.

  • Treat Safari's "Invalid call to runtime.sendMessage(). Tab not found." as teardown. It fires routinely while a page unloads, including right after a successful OAuth exchange, and forwarding it made the page bridge reject a promise nobody was left to catch. The relay now stops quietly and suppresses every later diagnostic.

  • Read __C2S_DEBUG at call time in all three templates, so logging can be turned on from a console that is already open instead of only by editing the build. Two new unconditional lines name the failures that used to be invisible: listeners that accept a message and never answer it, and a background that answers no ping on either transport.

  • Say why a blank panel is blank. A site's frame-ancestors list decides who may embed it and it is written for Chrome, so an extension page whose origin is safari-web-extension://<per-install UUID> can never be on it, and Safari computes the ancestor chain itself: no rule, header or proxy on this side reaches the check. A refused frame stays on its inherited about:blank, which is same-origin with the extension page, so the shim writes a short explanation into the frame and logs the site, the directive it read back, and who can actually change it. A frame that really loaded cross-origin reports contentDocument as null, which is what keeps this off working frames. See Safari Quirks E11 and Limitations and FAQ 2b.

Claude in Chrome 1.0.85 signs in end to end on this build. Its side panel works in the classic mode, which authenticates with the OAuth token this release repaired; the newer embedded mode cannot be framed and is the site's call, not ours. The earlier row covered the popup and the panel toggle only, which is why the bridge looked fine for so long. See Tested Extensions, OAuth Bridge, Safari Quirks E1/E9/E10/E11.

v1.10.3, 2026-08-18

Theme: the team detection widened in 1.10.2, checked against the Xcode versions other people actually run rather than the one on this machine.

  • Read ApplicationIdentifierPrefix out of a provisioning profile. Profiles only grew a TeamIdentifier key around Xcode 6; older ones carry the same id under the earlier key, and the profile source skipped them entirely (e0fba1a).
  • Accept Developer ID Application and 3rd Party Mac Developer Application identities. The keychain lookup took development and distribution certificates only, so a paid account that has never created a development certificate, which is every account that only ships notarized releases, read as "no Apple account" for the same reason as #14. The team id lives in the same subject OU on all of them. Development identities still win when both are present, since that is what the build asks for (e0fba1a).
  • The detection tests were rewritten to run against stub defaults / security / openssl binaries and a temp HOME, so they assert real behavior on any machine instead of skipping when the keychain has no identity. Twelve shapes are covered: both preference keys, both preference domains, quoted ids, both profile directories, three profile key layouts, and five certificate names (e0fba1a).

v1.10.2, 2026-08-18

Theme: a --team auto run that did everything right, told the user it had failed, and pointed them at a fix they had already applied (#14).

  • Stop failing the run for the ad-hoc fallback it announced itself. --team auto and plain --install mean "team-sign if a team is available", and when none was found the CLI said so up front and fell back. The signature check added in 1.10.0 then read the resulting ad-hoc bundle, decided team signing had been requested and not delivered, and exited 1 with "sign in to Xcode", which is what the reporter had already done. The expectation is now what actually reached xcodebuild, not what was typed on the command line: a concrete team that came out ad-hoc still fails, since the build ignored the signing it was told to use, but an announced fallback is a warning and exits 0. An unsigned bundle fails either way, because the fallback promises ad-hoc signing rather than no signature (a87574a).
  • Detect the team from more than Xcode's preference cache. detectXcodeTeam read IDEProvisioningTeamByIdentifier from com.apple.dt.Xcode, then a codesigning identity from the keychain. A freshly signed-in account has neither: Xcode writes that cache only once it has provisioned something, and it does not mint an Apple Development certificate until the first signed build, so signing in changes nothing the tool can see and the advice to sign in is a loop. It now also reads IDEProvisioningTeams and the com.apple.dt.xcodebuild domain (which key and which domain get written varies by Xcode version), and any provisioning profile under ~/Library/Developer/Xcode/UserData/Provisioning Profiles or ~/Library/MobileDevice/Provisioning Profiles, newest first. Profiles are CMS-signed but the payload plist sits in the blob as plain XML, so TeamIdentifier is scanned out of the bytes instead of shelling out to security cms -D per file. The id is the whole job, since the build already runs xcodebuild -allowProvisioningUpdates and Xcode mints the certificate itself (47d1ce1).
  • The fallback warning now names the two things that actually resolve it: Xcode → Settings → Accounts → Manage Certificates → + → Apple Development, which writes both the certificate and the cached team, or --team <TEAMID> from the membership page to skip detection entirely. Help text, README, and the wiki were updated to match (a87574a).

v1.10.1, 2026-08-17

Theme: the two Safari behaviors between a converted Kondo and a web app that can talk to its extension, both found by driving an extension a user had reported dead.

  • Emulate navigator.serviceWorker in an extension page. Chrome registers the MV3 background service worker against the extension origin, which is what makes the documented web-page-to-background channel work: the page hands an extension-origin iframe a MessagePort and the iframe forwards it with (await navigator.serviceWorker.ready).active.postMessage(msg, [port]). The conversion turns the worker into a background page, so nothing is registered for the origin at all — measured in a converted page, getRegistrations() comes back empty, controller is null, and ready stays pending indefinitely. The await never returns, nothing throws anywhere, and an extension whose only channel is that port sits there looking installed and doing nothing. Kondo is exactly that shape: app.trykondo.com signed in and the extension never answered a single request. ready now resolves with a registration stub and postMessage is tunneled to the background over a runtime.connect port, which is the only transport available here — an extension page embedded in a web page runs in the web content process, where extension.getBackgroundPage() is null (still null after 11 s with a port held open), so the port cannot be handed across realms the way the offscreen client is. A MessagePort cannot cross a runtime port either, so each transferred port is bridged by id and the background re-materializes a real MessagePort from its own MessageChannel, then dispatches a message event carrying it plus the source and waitUntil a worker handler expects. Payloads cross as runtime-message JSON, so Blobs, ArrayBuffers and Dates do not survive (0f37df7), see Safari Quirks C7 and Runtime Shim.
  • Return the cookie the request will actually send from cookies.get. When two cookies share a name on nested domains, Safari answers get({name, url}) with the parent-domain one while the network stack sends the host one first; Chrome breaks the tie by longest path then earliest creation, which returns the site's own cookie. So an extension reads a session value and then makes a request carrying a different value for that same cookie. LinkedIn's real JSESSIONID lives on .www.linkedin.com, Kondo mints a placeholder on .linkedin.com whenever it finds no session (which is what happens the first time it runs before LinkedIn has been opened in Safari), and from then on every voyager call went out with a Csrf-Token contradicting its own Cookie header and came back 403 CSRF check failed — permanently, since the junk cookie outlives the session that produced it. Kondo's own error payload carried both cookies and the token it sent, which is how the precedence was pinned down. get now re-resolves through getAll({url, name}) and, only when more than one candidate comes back, returns the one the server sees first (RFC 6265: longest path, then the most specific domain); a single candidate, an incomplete query, or a callback-only getAll falls through to the native get (3deb446), see Safari Quirks E8.

Kondo 1.12.1 joins the table as Working: the web app signs in and reaches Kondo's own plan-selection screen, linkedin.com/messaging auto-opens the inbox, and the toolbar button's contexts:["action"] menu carries its own toggles with the right one ticked. What sits behind Kondo's paid plan is unverified rather than known broken. Its empty toolbar popover is not a conversion fault either: the popup only does window.open(app); window.close(), the tab opens, and Safari refuses to let script close the popover it drew. See Tested Extensions.

v1.10.0, 2026-08-17

Theme: two Safari behaviors that silently swallow an extension's requests for its own files, and a signing path that stopped taking its own output on trust.

  • Lowercase the host on the empty getURL("") only. Safari serves getURL("") with an uppercase UUID host and reports every origin (sender.origin, new URL().origin, location.origin) lowercase, and bundles gate on both idioms. v1.9.1 aligned sender.origin up to getURL's case for uBlock's sender.origin === getURL("").slice(0,-1), which broke the mirror-image check: LastPass's background accepts its popup's initialize message only when new URL(sender.url).origin === sender.origin, always lowercase, so it never matched and the popup sat on its loading splash. Resource paths keep Safari's real case, since the resource server is case-sensitive on the host and a lowercased fetch(getURL("manifest.json")) 404s, and Honey and Dark Reader match location.href against getURL("/…"). The sender-origin rewrite becomes a no-op (929c2ca), see Safari Quirks.
  • Rewrite the scheme on chrome-extension://__MSG_@@extension_id__/…. That is how a bundle reaches its own files from CSS, and Safari substitutes the per-install UUID for the placeholder while leaving the scheme alone, so the request leaves an https page as chrome-extension://<uuid>/… and WebKit blocks it: "requested insecure content … must be served over HTTPS". Cloaked's content.css lost 18 webfonts on every injection into my.cloaked.com, which is also what buried its console. CHROME_SCHEME_RE skips these and has to, because its host guard cannot separate a placeholder from a real id and a concrete-host URL may be an OAuth redirect_uri registered with a provider; a placeholder host never is, since nothing outside the browser resolves it. walkScripts takes an extension list now, so this pass reaches CSS and HTML that the script-only rewrites never opened (053e525), see Conversion Pipeline and Safari Quirks.
  • Verify the signature instead of trusting team detection. Detection only predicts how a build will be signed; when it came up empty the build fell back to ad-hoc behind a single warning line, so the extension installed, worked, and disappeared on the next Safari quit. The signature is now read off the built .appex with codesign -dvv and held to what the run asked for, so no certificate, a stale copy of the tool, or xcodebuild dropping the identity all surface the same way. Behavior change: a run that asked for a team (--team <id>, --team auto, or plain --install) and produced an ad-hoc bundle exits non-zero, and not behind --verify — a build that ignored the signing it was told to use did not do what was asked, the same as an install that does not land. Builds with no --team are untouched (f24972c, tests in 118c327), see Build and Install.
  • Corrected the help text and README, which still described Xcode's preference cache as the only source of a team id after the keychain fallback landed in v1.9.0 (f24972c).

v1.9.1, 2026-07-31

Theme: four shim bugs between a converted Honey and a popup that renders, two of which viaduct had caused itself.

  • Only give a real side panel the injected tabId param. Safari opens a side panel as a popover without the ?tabId=<n> a panel page expects, so the shim resolves the active tab and writes it in with replaceState. That ran for plain action popups too, where Chrome puts no query on the URL either, so the page was handed a URL its own code never expects. Honey picks which message service to send on by comparing location.href to getURL("/popover/popover.html"); once the query landed the compare failed, every popup RPC went out as messages:cs, and the background's messages:cs listener bails on any sender without a sender.tab, which is every message a popup sends. No reply, no error, empty popup. This is also where Dark Reader's popup was getting the ?tabId that 1.8.0 and 1.9.0 strip back off, so those fixes were treating our own symptom (064812b), see Safari Quirks F3 and Runtime Shim.
  • Give getURL one host case, and fix sender.origin instead. Safari reports the extension UUID uppercase in getURL, location.href and sender.url, and lowercase in sender.origin, and bundles compare getURL's output against both. The root args were lowercased so uBlock's sender.origin === getURL("").slice(0,-1) would match, which broke Honey's location.href.includes(getExtensionURL("/")): its popup uses that to decide whether it is the popover, and with it false it treated its own href as the current page URL and sent every message without a tabId. Resource paths could not be lowercased at all, since Safari's resource server is case-sensitive on the host and a lowercased fetch(getURL("manifest.json")) 404s. getURL now returns Safari's case for every argument and sender.origin is aligned to it on the clone the sender fix already hands listeners, which is where mutation actually sticks. The sender.url half of the old lowercasing had already gone in 43047af (bf014e0), see Safari Quirks B4 and Runtime Shim.
  • Emulate tabs.onActivated, because Safari never fires it in the background. Probed live with logging wrapped around the registrations: both tabs.onActivated and windows.onFocusChanged are registered 28 ms into boot, the user clicks between three tabs, and nothing arrives, while that same background is demonstrably alive and answering popup messages 8.7 s later. An extension that keeps the selected tab in a variable fed only by those events never learns one; Honey's getSelectedTab() is tabs.get(selectedTabId), so its popup's opening request made the background call tabs.get(undefined) and Safari answered "Invalid call to tabs.get(). The 'tabID' value is invalid, because a number is expected." The shim polls the active tab in the background while something is listening. The first poll always dispatches, since Safari restarts the background constantly and a woken one has missed every activation before it, and one real onActivated stops the polling permanently so a future Safari cannot deliver everything twice (7b1b69a, hardened in 61cf026), see Safari Quirks C6 and Runtime Shim.
  • Strip the query off a relayed sender.url too. Native delivery already did. The storage relay builds its sender out of location.href, so an extension page whose URL carries a query reported a sender.url that no longer equalled getURL(path), and bundles allow-listing their own pages by exact match dropped those messages without ever calling sendResponse. Dark Reader's popup sat on "Loading, please wait" with dead buttons (11a00b4), see Safari Quirks B2.

Honey 19.4.0 moves to Working on this build: popup renders and a session signed in on joinhoney.com is recognized. Coupon application at checkout is unverified. See Tested Extensions.

v1.9.0, 2026-07-29

Theme: the messaging paths a content script depends on, driven by the Cloaked and Tampermonkey conversions, plus chrome.userScripts reaching the page at all.

  • Fall back to the signing certificate when Xcode's team cache is empty. detectXcodeTeam read only IDEProvisioningTeamByIdentifier, a preference Xcode writes asynchronously and leaves missing on machines whose Apple account signs perfectly well, so viaduct reported no account and quietly dropped to ad-hoc signing, which Safari disables again on every quit. The team id now comes off the codesigning identity in the keychain when the preference is empty, read out of the certificate subject's OU (168d505), see Build and Install.

  • Correct the background-context check in converted background scripts. An MV3 bundle decides "am I the background?" by the absence of window, which holds in Chrome because the background is a service worker and stops holding here because the conversion gives it a background page. The bundle then concludes it is a content script and everything keyed off that identity misroutes without a word. Cloaked's crx-kit dispatcher opens with if (msg.to !== this.myEndpoint) return false, so {to:"BACKGROUND", name:"openAuthUrl"} arrived at a listener that dropped it and Log in spun forever. Rewritten only in the files the background itself loads, and only where the check directly produces a background-ish value, so a bundled library using the same idiom to pick a Node path is left alone, as is the identical detector inside the popup bundle, which has to keep answering "foreground" (cd58700), see Safari Quirks A9 and Runtime Shim.

  • Emulate webNavigation.onHistoryStateUpdated from the content script. Safari has no implementation and the shim backfilled an inert stub, which looks exactly like a real event that has not fired yet. Cloaked installs its page-to-extension bridge from this event on my.cloaked.com, a Vue app, so after login the dashboard's postMessage had nobody listening. Safari re-injects content scripts on same-document navigations with a fresh isolated world each time, so the sender cannot remember its previous URL; it announces the current one on every injection and the background, which does persist, holds the per-tab baseline and decides what changed. Between injections the URL is sampled in short bursts after user input rather than on a standing interval, since a permanent timer in every frame of every page is a real cost for a rare event (0949657), see Safari Quirks C5 and Runtime Shim.

  • Populate tabs on the Window returned by windows.create. Chrome guarantees that array on the created window; Safari leaves it undefined, and an extension that reads the new tab straight off the result treats that as a hard failure. Cloaked's auth flow throws "Created window has no tabs available" after its popup has already closed itself, so the user watches a spinner disappear and no login window arrive. The wrapper backfills from a tabs.query on the new window and synthesizes a tab when the query comes back empty, because callers index tabs[0] unconditionally and an empty array is as fatal as a missing one. It also retries without type when Safari refuses a window type it does not render (7fd15e8), see Safari Quirks C4 and Runtime Shim.

  • Stop swapping the extension page's global api object in the storage relay. The relay replaced chrome/browser on extension pages with a Proxy to intercept sendMessage/onMessage, and Safari answers that by not delivering content-script messages to the page at all, including to a listener registered on the pristine native event before the swap. So it is not a registration problem: Safari resolves delivery through the page's current global at dispatch time. Every converted extension whose content scripts message the background was silently dead, Cloaked included, since its login tokens are read by a content script and sent to the background. Replacing runtime.onMessage in place fails the same way; wrapping its addListener does not, so the relay records listeners that way and leaves both identities alone. Bisected across six converted builds (8acef29), see Safari Quirks E7 and Runtime Shim.

  • Keep content scripts and MV2 backgrounds on native messaging. Safari only delivers a content script's native runtime.sendMessage to the background, and only lets its sendResponse round-trip, when both sides sit on the real unwrapped runtime, so the relay's Proxy was dropping the delivery in two contexts that never needed the relay: content scripts, and the MV2 background the shim was being prepended into. TWP was the visible casualty, sending {action:"translateHTML"} and waiting on a reply that never came. The relay still runs where Safari genuinely drops native messaging, which is extension pages: popover, panel, options (4010230), see Runtime Shim and Safari Quirks E6.

  • Make chrome.userScripts actually reach the page. The registry was coherent and inert, which is fine for an extension that only manages scripts and useless for one whose whole injection strategy is this API: Tampermonkey declares no content_scripts at all, so its userscripts saved, listed as enabled, and never ran. The background now publishes its registry to storage.local on every register/update/unregister, and viaduct injects a content script (only into extensions that declared the permission) that reads it at document_start and evaluates what matches the page. Storage rather than messaging, because runtime.sendMessage broadcasts to every listener and the first sendResponse wins: Tampermonkey's own background handler consumed the request and answered with nothing on all twelve retries, and storage additionally survives the background being torn down. Tested end to end in both halves (62ec130), see Safari Quirks C3.

  • Append a sacrificial message to every _locales/<locale>/messages.json during staging, because Safari doesn't load the final entry of a locale catalog. getMessage returns "" for that key in every form, and the extension then renders its raw message key. Tampermonkey's last message is v0version0 ("v$version$"), so its dashboard header showed a literal v0version0 instead of the version while top_level_await immediately before it resolved fine. Ruled out the two obvious explanations live: entries past the same byte offset resolve, and other placeholder messages resolve, so the entry is dropped purely for being last. The insert is textual so shipped formatting and escapes survive byte for byte (937da18), see Safari Quirks A7 and Conversion Pipeline.

  • Keep the DNR rules Safari accepts when one rule in a dynamic batch is invalid. Safari rejects the whole update{Session,Dynamic}Rules call if any single rule fails validation, so one uncompilable regexFilter costs the extension every other rule it tried to register. Tampermonkey registers all of its *.user.js interception rules in one call, so userscript-URL detection was dead with only a console error to show for it. The wrapper now re-applies the removals and adds rules one at a time, keeping what lands; a batch where nothing lands still surfaces the original error (479d518), see Safari Quirks D2.

  • Deliver client.postMessage to the emulated offscreen document. The shim fabricates self.clients for extensions that probe for their offscreen document the Chrome way, but the fabricated client's postMessage was a no-op. That satisfies an existence probe and breaks the other half of the contract: the SW-to-offscreen binary handshake posts a message with a transferred MessagePort and awaits the reply on its twin, so the await never settled. Nothing threw, so it read as a hang with a clean console. Tampermonkey hit it on every editor save (it wraps the script source in an object URL created offscreen), which is why saving a userscript spun on "Please wait..." and then reported a bogus "Unable to parse this!" (3dd05f2), see Safari Quirks C2 and Runtime Shim.

  • Stop reporting a dynamic-declarativeNetRequest extension as a blocking-webRequest content blocker. The class-level error exempted only extensions shipping static declarative_net_request.rule_resources, so an extension that declares the DNR permission and builds its rules at runtime (updateDynamicRules / updateSessionRules) still tripped it and conversion aborted. Tampermonkey is exactly that shape (webRequestBlocking + <all_urls> + declarativeNetRequestWithHostAccess, no static ruleset), so a userscript manager was blocked as an ad blocker. Since the transform already maps the Safari-rejected WithHostAccess token back onto plain declarativeNetRequest, the DNR path survives conversion, and the exemption now follows the permission rather than the ruleset (3403ff7), see Analyzer and Manifest Transform.

  • Don't hijack a setPopup-driven toolbar button with the action-click bridge. The bridge treated any background that mentioned onClicked next to action/browserAction as an onClicked-driven button and injected a transparent stub default_popup. But an extension can register onClicked and still wire its real popup at runtime via setPopup, which Safari honors, so the stub overrode it and the extension's own popup never opened. TWP (Translate Web Pages) is exactly this shape: it sets popup/popup.html via setPopup at startup and its onClicked fires only under a non-default config, so clicking the toolbar button showed an empty gray stub and there was no way to start a translation. backgroundRegistersActionOnClicked now scans all background files collectively and opts out when any of them sets a non-empty popup via setPopup; an empty setPopup({popup:""}) clears the popup and doesn't opt out. The collective scan matters because viaduct prepends its own shim (which references onClicked + action but no setPopup) to background.scripts, so a per-file check would short-circuit on the shim before reaching the bundle's real setPopup. Fixed in both detector copies (6f63db2), see Runtime Shim and Safari Quirks A5.

Theme: an adversarially-verified sweep over the whole pipeline — eight correctness/robustness fixes, none behavior-visible on the happy path. All land in 92fa2ee.

  • Recover the CRX-embedded key with the lenient parseJsonc, not strict JSON.parse. A Chrome manifest carrying a BOM, // comment, or trailing comma made JSON.parse throw, so the key was silently dropped and the derived Chrome id (OAuth bridge, chrome-extension:// origin spoof) was lost for exactly the bundles most likely to be hand-edited (92fa2ee), see Input Handling and OAuth Bridge.
  • defaultBundleId now hashes whenever the slug isn't a lossless rendering of the name, not only when it's empty. Two distinct names reducing to the same non-empty slug (Foo/1Foo, Cafe/Café, MyExt/My-Ext) previously collapsed to one bundle id and shadowed each other in LaunchServices (92fa2ee), see Build and Install.
  • --uninstall reads the installed app's real CFBundleIdentifier to key the native-messaging broker LaunchAgent, instead of recomputing the default id. An extension installed with a custom --bundle-id used to leave an orphaned KeepAlive agent running open on the deleted app forever (92fa2ee), see Build and Install and OAuth Bridge.
  • The whole-download 5-minute wall-clock deadline is threaded through the redirect recursion as one shared deadline, instead of re-armed per hop — a 5-hop redirect chain could otherwise stay open ~30 min (92fa2ee), see Input Handling.
  • The ancestorOrigins guard regex no longer allows )/] in the receiver walk-back, so it can't start a match mid-expression (foo().ancestorOrigins…) and emit unbalanced parentheses. Latent (real bundles never call through to ancestorOrigins), but a token rewrite must never be able to produce invalid JS (92fa2ee), see Safari Quirks E3.
  • The action hotkey maps a named command key to its DOM KeyboardEvent.key form (UpArrowUp, Space" ", …) instead of the bare WebExtensions token, and falls back to the Ctrl+Shift+Y default when a token is unmappable. A command like Ctrl+Shift+Up used to wire a shortcut that could never fire (92fa2ee), see Runtime Shim and Safari Quirks A5.
  • Drop the dead try/catch around Buffer.from(key, "base64") in deriveChromeId — Node's base64 decoder never throws; the der.length === 0 check is the real malformed-key guard (92fa2ee), see OAuth Bridge.
  • Strip comments before the importScripts dynamic-argument residue test, so importScripts("a.js" /*x*/, "b.js") is recognized as fully static instead of being flipped to dynamic — which needlessly triggered webpack-chunk collection and module-mode background loading (92fa2ee), see Runtime Shim and Conversion Pipeline.

Regression tests added for the bundle-id collision, the hotkey key mapping, and the ancestorOrigins call-receiver balance.

  • Capture content-script runtime.onMessage listeners unconditionally so the in-page action hotkey works. The hotkey replays the extension's toggle message to the listeners the shim captures in the content world, but that capture was gated on a __vHotkeyWired flag derived from chrome.runtime.getManifest().content_scripts. Safari strips content_scripts from getManifest() inside a content script, so the flag read false there, the capture never installed, and the hotkey fired into an empty list. SuperDev Pro's Cmd+Shift+S sidebar toggle did nothing as a result (its onClicked and commands.onCommand are both dead in Safari, so the hotkey is the only path). Dropped the getManifest-derived gate; the capture now always runs in a content-script context, verified live (190133d), see Runtime Shim and Safari Quirks A5.
  • Stop overwriting an extension's own browser-polyfill.min.js. uBlock Origin ships its own polyfill in its content-script list; writePolyfill copied viaduct's webextension-polyfill build over that exact file, so uBlock's vapi.js (compiled against its own copy) threw at load and every content script after it, including contentscript.js (the cosmetic/element-hiding engine), never ran. The extension installed, enabled, and badged from the background but did nothing on the page. viaduct now writes its copy under viaduct-browser-polyfill.min.js when the extension already ships one, leaving the original intact and loading both (the polyfill is idempotent) (622001c), see Runtime Shim and Safari Quirks E5. Closes #10.
  • Strip a chrome://-scheme URL (and any other unparseable-scheme host pattern, e.g. ws://) from permissions / optional_permissions during transform. Tampermonkey ships chrome://favicon/ in permissions; left in place, Safari treats the whole manifest as invalid and never loads the extension, even though it installs and registers with pluginkit. The analyzer already warned but the converter left it alone (warn-don't-move), which is right for a merely-ignored https:// pattern but fatal for a Chrome-only scheme Safari can't grant anywhere. The transform now drops these the same way host_permissions already filters unparseable schemes, and the analyzer reports the case as auto-fixed (31d13b9), see Manifest Transform and Safari Quirks A6. Closes #11.

v1.8.0, 2026-07-23

Theme: Safari runtime-compat fixes, driven by the TWP – Translate Web Pages conversion (#4) and a review-surfaced XHR bug (#6). Squash-merged as #5 (7247e8b) and #8 (47d2454); per-fix shorthashes below are the working-branch commits.

  • Route content-script messages and XHR through the correct Safari paths (26605b7), see Runtime Shim.
  • Stop hijacking onClicked actions and preserve version_name in the transformed manifest (5b1bd03), see Manifest Transform.
  • Guard ancestorOrigins reads and rewrite self-page chrome-extension:// URLs during staging (02c4007), see Safari Quirks E3 / B1.
  • Prepend the compat shim to MV2 background scripts so the background survives load (0240e1d), see Safari Quirks A1.
  • Strip ftp:// from contextMenus URL patterns (4a5cae9), see Safari Quirks F4.
  • Cap commands at Safari's 4-shortcut limit (d3fcb9a), see Manifest Transform / Safari Quirks A4.
  • Make top-level content-script globals re-injection-safe (const/letvar) so Safari's double-evaluation of a document_end/all_frames group into a shared world can't throw "Can't create duplicate variable" (1f22a10), then normalize ./ and backslash content-script js paths so every referenced file is reached (2dc30ca), see Safari Quirks E4. Closes #4.
  • Proxied XHR fires timeout (+ loadend), not error, on a timeout (2501a8b), see Safari Quirks D6. Closes #6.

v1.7.0, 2026-07-21

Theme: debugger/CDP emulation, native-messaging broker, real Chrome-id derivation, plus licensing.

  • Emulate chrome.debugger / the Chrome DevTools Protocol for converted extensions (a2a0f55), see Runtime Shim.
  • Native-messaging broker so runtime.connectNative works on Safari (05cc244), see OAuth Bridge.
  • Derive the Chrome extension id from the CRX public key instead of guessing (34d4724), see Build and Install.
  • Source-comment tidy (f7e3c75).

Licensing / meta milestones folded into this release window:

  • Added an MIT license and privacy policy (8579c84), then a security policy (3c94a79).
  • Relicensed under PolyForm Shield 1.0.0 (a8bd84d), the current license.

v1.6.0, 2026-07-18

Theme: main-world content scripts.

  • Re-declare content-script page-world injections as world: MAIN so scripts that expect the page's own JS context behave on Safari (d35e3ad), see Manifest Transform and Safari Quirks.

v1.5.0, 2026-07-18

Theme: popup-less action buttons.

  • Popover-free toggle for popup-less onClicked actions, so a toolbar button with no popup fires its click handler on Safari instead of opening an empty popover (e9ba1e3), see Safari Quirks.
  • Analyzer now flags blocking-webRequest content blockers that can't block in Safari (dbe3442), with README guidance that full uBO can't block and users should reach for uBO Lite (644e97e), see Analyzer.
  • deriveAppName whitelists characters to stop XML/scheme injection into generated project files (ba89cc7).

v1.4.8, 2026-07-14

Theme: URL-rewrite correctness fix.

  • Keep concrete-host chrome-extension:// URLs intact rather than rewriting them (1b7b6f5), see Safari Quirks.

v1.4.7, 2026-07-13

Theme: docs.

  • Spell out the Safari website-access grant step users must perform after install (2ea6657), see Build and Install.

v1.4.6, 2026-07-12

Theme: Safari blob downloads, dark mode, webpack SWs, popup tightening.

  • Safari blob downloads, dark-mode pages, webpack service workers, and WebSocket hosts (7acf2ca), with follow-ups to preserve the filename via the share sheet (bfa7903) and to make direct one-click blob downloads keep the correct extension (6c2396d).
  • Route offscreen-document replies and rewrite the chrome-extension: scheme (5a12186); strip the ?tabId query from popup sender.url (49c41cb), see Runtime Shim.
  • Keep a declarativeContent-gated action button clickable and further tighten popup sizing (db06848), see Safari Quirks.

v1.4.4, 2026-07-06

Theme: docs / release chore.

  • Smaller viaduct ASCII icon in the README (39820ba); release bump (886284c). No code changes.

v1.4.3, 2026-07-06

Theme: docs / release chore.

  • New viaduct ASCII icon and README banner update (357a49e); release bump (a62f069). No code changes.

v1.4.2, 2026-07-05

Theme: bug-hunt hardening at scale, plus config/batch/--verify.

This is a large release built mostly from multi-agent audit passes, hundreds of small correctness fixes across the shim, manifest transform, OAuth templates, and analyzer accuracy:

  • Config file, batch convert, and --verify mode (33e9bce), the batch/verify feature landed here, see Build and Install.
  • Wire 4 previously-inert shim stubs to real Safari/web implementations (94ad310); strip two Safari-unrecognized permission tokens (774d0a8), see Runtime Shim and Manifest Transform.
  • Shim runtime hardening, frozen-root throw guards, storage.session correctness, missing runtime APIs (f344d7a); external-messaging events and proxy-config substitution guard (54172b4).
  • Resolve __MSG_*__ i18n placeholders in the generated background.html title (f02a0cc), see Manifest Transform.
  • Rolled-up audit fixes: 6 bugs (e9150d8), 9 bugs (4f42d8f), 5 issues (23aef5e), 13 issues (b016620), 8 more shim gaps from corpus cross-reference (8f8aef6), 22 issues across runtime/CLI/analyze (9a221ce), 13 bugs incl. iOS build/crashes/OAuth/codegen (eb71af6), and 28 bugs incl. shim semantics/manifest/OAuth/analyze (6e0d73b).
  • Single source of truth for the default Safari min-version (45f4d29); removed the scratch scripts/ and docs/ dirs (7f971ad); new README ASCII icon (dd17de2).

v1.4.1, 2026-06-24

Theme: sidePanel + action badge fixes.

  • Safari sidePanel toggle and action-badge init-abort fixes, with 5 extension test reports (4d1109a), see Safari Quirks.
  • 14 correctness bugs from a deep multi-agent codebase audit (e5d343b).

v1.4.0, 2026-06-24

Theme: many audit fixes, the Grammarly port-routing rewrite, and the src/ reorg.

The headline work is getting real extensions (Grammarly, uBlock) to actually run on Safari, driven by the port-routing rewrite:

  • Rewrite chrome.runtime.id to the UUID host and strip the runtime.id prefix from port-routing matchers so Safari routes popup↔background ports correctly (362feb9, cc611a3), then remove the now-dead port-clone machinery the matcher rewrite superseded (43047af), see Runtime Shim. Root-cause chain documented in b05abd8.
  • Supporting shim fixes for the same goal: route popup port by lowercasing sender.url host (e90a45f); forward cloned-port methods bound to the real port for Safari's brand check (7c39c87); defineProperty-wrap native onConnect/onMessage/cookies events (16eccc6); backfill storage.session.setAccessLevel and clear runtime.lastError so background init can't hang or reject (e0a70ea, f3eb880); carry the httpOnly session cookie through the native-host auth proxy (6071347); guard cookies.onChanged against Safari null events (8952cd5); restore resource loading + connect-src 'self' for Grammarly's background init (7debbf8).
  • Make converted extensions survive Safari's frozen/exotic namespaces and no-chrome globals; backfill MV3 enums/storage/userScripts events on the resolved namespace (73c1926, ca523e8, b81dda0, 1880790); blocking webRequest degrades instead of aborting conversion (aa89a20), see Runtime Shim and Analyzer.
  • Input: descend into the lone manifest-bearing subdir of a repo-style layout (e64f335); balanced-paren importScripts neutralization + DNR path-escape guard (e8830e0).
  • Large refactor splitting src/ into concern folders input/manifest/analyze/runtime/build (a07abde), extracting the shim runtime JS to src/runtime/safari-compat-shim.js (2927cdf) and the Safari compat-data tables out of manifest.ts (3f919d0); added CLAUDE.md architecture docs (41779ff).
  • Final rollup of audit correctness bugs across shim/analyze/manifest/build (d885422); the release also carries per-extension Safari test reports and a live-testing debug protocol (7bd822d).

v1.3.1, 2026-06-21

Theme: UA-sniffing detection + a bug hunt.

  • Detect navigator.userAgent Chrome-version sniffing so the analyzer can warn about it (a534d69), see Analyzer.
  • Surface shim-emulated APIs in analysis output and fix pluginkit parsing (30509f8), see Analyzer.
  • Bug-hunt fixes: 3 conversion bugs (cef6aee); bundle-id collisions + a symlink leak in staging (b0c81a1); native-proxy Response crashes on out-of-range/null-body status (3a16e42); shim fill() no longer clobbers legitimately-falsy native members (273272b); sanitize hosts/origin in generated native-proxy Swift literals (dab04f1); fix report handling of the MV2 page_action fold (72158b8).

v1.3.0, 2026-06-20

Theme: emulate complex Chrome APIs behind a catch-all shim safety net; keyboard-shortcut management.

  • Emulate complex Chrome APIs and add a catch-all shim safety net so unknown chrome.* access degrades gracefully (75d6cd2), see Runtime Shim.
  • Make keyboard-shortcut management work in Safari, which has no chrome://extensions/shortcuts page (503e14f), and warn at build time on hardcoded chrome://extensions/shortcuts (or settings) links (f74597a); documented in the shim feature list (e7d1653), see Safari Quirks.
  • First real test suite lands and hardens the release: applyDnr (7e8477d), analyzeManifest CSP/host-misplacement (1156251), scanExtension detections (2cc6c34), extract.ts CRX3/zip-slip (748a9eb), plus a DNR keyless-ruleset fix and sidepanel-open test (cd9b73c).
  • GitHub issue templates for conversion failures, broken extensions, and feature requests (ed8be98, later trimmed in 2d7b4ac).

(This tag range also sweeps up the tail of the README ASCII-banner iteration 700c32185e408c and the final publishing-pipeline commits 1d728d247b3dea, see the v1.2.x note below.)


v1.2.8, 2026-06-19

Theme: the rename to viaduct, @magicelk235 scoping, and the Trusted-Publishing burst.

This is where chrome2safari became viaduct and npm publishing was rebuilt:

  • Rename chrome2safariviaduct across README, package, CLI, and source (6624163, c497683); scope the package as @magicelk235/viaduct and add a release publish workflow (aaa1437); switch to npm Trusted Publishing (OIDC) instead of NPM_TOKEN (df4346f), see Build and Install.
  • Publishing-pipeline hardening (one note for the whole burst). Standing up OIDC Trusted Publishing took an iterative sequence of CI/npmrc fixes, 1197fd9 (bump past the burned 1.2.6 publish), a983d3a, 2a8ce3f (run on macos-latest, the package is os:darwin), cca58cd (shell-expanded test glob), 1d728d2 (pin node, verify npm version), 01cc7a7/8073d4e/0dadd91 (registry-url and .npmrc token-auth juggling so OIDC isn't shadowed), 6038846/47b3dea (diagnostics added then removed once proven). The 1.2.6/1.2.7 versions were consumed pre-tag; v1.2.8 is the first tag published via Trusted Publishing.
  • Shim/analyzer work carried in this window: shim chrome.windows/devtools/app plus a CORS proxy and build-config hardening (8255ebc); first test suite + CI gate with corpus-run bug fixes (0dd9519); oauth-bridge idempotency, CSP sandbox scoping, optional-perm host warning, CDP MAIN-world eval (0fe0447), see OAuth Bridge; derive extension id generically instead of hardcoding Claude's (208eaa6); two converter-hardening sweeps (0ed107a, 7395bec); flag native-messaging usage for Safari (a9c2e80).
  • Manifest/CLI/analyzer additions earlier in the window: manifest compat transforms + validation + input hardening (b20954b); CLI lifecycle flags and new compat checks (a40114e, 24424b4); complete chrome.i18n and harden the runtime shim (1f3af6f); harden staging/extraction/reporting (781b01c).
  • Popup sizing on Safari iterated toward "floor only, never overrides the app's own size" (0601904, b33698a, fb05734, bf77318), see Safari Quirks. Service-worker importScripts hoisted into the background page for a CSP-safe fix (fc49074).

v1.2.0, 2026-06-12

Theme: DNR sanitize, broaden the shim, unified scanner + i18n, packaging robustness.

  • Sanitize static DNR rulesets for Safari (55d8b0e), see Manifest Transform.
  • Broaden chrome.* polyfill coverage and fix runtime gaps (a69e926), see Runtime Shim.
  • Unified scanner with i18n name resolution and richer checks (744822b), see Analyzer.
  • Packaging robustness and lockfile sync (f415d0c); publish-workflow token fix (7dc63b0), and a post-tag URL fix for provenance validation after the repo rename (e1d9317).

v1.1.0, 2026-06-08

Theme: expanded compat analysis, stub no-equivalent namespaces, bundle the polyfill.

  • Expand Safari compatibility-analysis coverage (0d9e8e4), see Analyzer.
  • Stub no-equivalent chrome.* namespaces in the compat shim (1232fff), see Runtime Shim.
  • Bundle webextension-polyfill for browser.* parity (67e2039).
  • CI to auto-publish to npm with provenance on version tags (45e6366), the first publishing automation, later replaced by OIDC in the v1.2.x burst.

v1.0.1, 2026-06-06

Theme: the initial converter, made npm-publishable.

The 1.0 line predates tags, it runs from the scaffold at 2fd033c through the v1.0.1 tag. This is the whole first-cut pipeline:

  • TypeScript CLI scaffold and workspace hygiene (2fd033c); shared types and process/IO utilities (ac8f154).
  • zip/crx/dir extraction with xattr cleanup (3846882); Safari manifest analysis/transform and a JS API scanner (5103497), see Manifest Transform and Analyzer.
  • The runtime compatibility shim, storage.sync, identity, sidePanel, notifications (530a7ed), see Runtime Shim.
  • Clean asset staging into a persistent output dir (7f0e825); packager wrapper with built-.appex bundle-id verification (e3e3007); pipeline orchestrator, CLI, reporting, and Safari 18 temp-load (c212d81), see Build and Install.
  • Safari OAuth bridge, identity polyfill + page bridges (53b8e3c), see OAuth Bridge; DNR ruleset to pin Origin for api.anthropic.com (5d1d542); a second runtime shim pass with a chrome.debugger polyfill and SW→background-page conversion (2b09dd6).
  • Safari fixes that made the output actually load: extension discovery + popup compatibility (a3a7ef5), persistent install via team signing + crash fixes (1a6ecef), MV3 non-persistent background + move-not-copy of the built app (bd3bbc7), and the analyze exit code / empty-action injection / debugger.onDetach fixes (146b6ca).
  • Made the package npm/npx-publishable (3013af2) and cut the release (c996214).

Clone this wiki locally