-
Notifications
You must be signed in to change notification settings - Fork 3
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
Theme: debuggable conversions, and the two Safari behaviors between uBlock Origin and a usable install.
- Add
--debugconversions and a--logsreader (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 tostorage.localunder__viaduct_debug_log__; Safari keeps that store as SQLite on disk, soviaduct --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
senderslot is a read-only exotic getter (59d65b7). The message path hands its corrected sender clone to listeners as an argument;onConnectdelivers the Port itself, so the oldport.sender = fixedassignment silently bounced off and connect-time privilege gates saw the raw value — uBlock's popup port never passedorigin === 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 insenderalone; non-own-origin senders pass through native. See Safari Quirks B4. - Normalize
webRequestframe types, anchored on the event prototype (64a8ab9). Safari labels every frame's document loadmain_framewith a raw 64-bitframeId, 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 assub_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-bootonResponseStarted. See Safari Quirks D4a. - The analyzer stops claiming
userScriptsregistrations are never injected (ef96792), untrue since the 1.9.0 emulation; the copy now names the real gaps (document_starttiming, the USER_SCRIPT-world CSP exemption). A conversion-regression workflow (9e92dd1) runs typecheck, the suite, and an--analyze --jsongate over synthesized MV2/MV3 fixtures on every push. The README was restructured and documents--background-launch,--config,--list, batch mode, and--strictwith--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.
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 globalbrowser, thenchrome, and casts what it finds, and the cast only succeeds on Safari's own namespace object. ThewebRequestfilter sanitizer used to republish both globals asObject.create(native)roots whenever Safari refused an in-place swap of thewebRequestslot, and from that moment Safari skipped the context: listeners stayed registered, senders resolvedundefinedwith nolastError, nothing was delivered. Live, that was TWP's content script asking the background to translate, so no page ever translated. The sanitizer now patchesaddListeneron 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.createthrows 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_actionnow map toactionas current WebKit does, Chrome OS-onlylauncheritems are skipped, andfile://*/*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 paintedhtmlas well asbody, and an extension that picks its theme in JavaScript rather than CSS looks light-only to the detector. A rule like TWP'shtml *{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 paintsbodyonly — a transparenthtmlpropagates 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.
Theme: installing without the host app taking the screen.
- Add
--background-launch(c74bd2b). Installing has always finished with a plainopenon 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 throughopen -g -jinstead: 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-gtreatment. Nothing changes unless the flag is passed. See CLI Reference.
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
launchWebAuthFlowopened its tab and then attached its ownwebNavigationlisteners, 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 bywebNavigation,tabs.onUpdated, and a 250ms poll oftabs.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.sessionacross 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 intostorage.localand 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.onStartupnever 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"onwindows.createand 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.
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.
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 ofstorage.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.sessionis the same object asstorage.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.
Theme: a build that failed and would not say why, reported against Yomitan (#15).
- Print the diagnostics
xcodebuildactually wrote. It splits a failure across both streams: theerror: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. Theerror:lines from both streams are now collected, de-duplicated across targets (each signing failure is repeated once per target), and capped. - Stop
--team autofrom 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 onerror: 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 automeans "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/.zipto 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.
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
ServiceWorkerGlobalScopeis 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 instorage.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 inviaduct-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, andabortOnLoadForNonInteractiveends the attempt as soon as the auth page loads anywhere other than the redirect target.
See Safari Quirks E12 and E13.
Theme: the one SPA navigation a converted extension never heard about, reported by a Cloaked user whose login said it had worked.
- Watch
location.hrefcontinuously in a content script where the extension actually readswebNavigation.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-worldhistory.pushStatealso 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 theextension-authstatus route once the token exchange returns, seconds after the last click: the bridge was never installed, the dashboard'sEXTENSION_AUTH_COMPLETEhad 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 righttabIdandframeId: 0, and the background'stabs.sendMessageback to that tab received and answered. See Safari Quirks C5, Tested Extensions. - The analyzer stops advising what the shim already does.
webNavigation.onHistoryStateUpdatedwas 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 aninfo[shimmed]finding that names the real remaining limits (a hidden tab, a subframe, an ungranted site).
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 nochrome.runtime, messaged nobody, and left its login button spinning while the relay, the background and the polyfill all reported healthy.applyOAuthBridgehas madepage-bridge.jsweb-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 whatworld:"MAIN"was for, and that case says so. -
Raise
strict_min_versionto 18.4 forworld:"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
sendMessageinto 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 oversendMessage, then three times over the shim'schrome.storage.localmailbox, 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 whoseoriginis 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_DEBUGat 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-ancestorslist decides who may embed it and it is written for Chrome, so an extension page whose origin issafari-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 inheritedabout: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 reportscontentDocumentasnull, 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.
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
ApplicationIdentifierPrefixout of a provisioning profile. Profiles only grew aTeamIdentifierkey around Xcode 6; older ones carry the same id under the earlier key, and the profile source skipped them entirely (e0fba1a). - Accept
Developer ID Applicationand3rd Party Mac Developer Applicationidentities. 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 subjectOUon 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/opensslbinaries and a tempHOME, 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).
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 autoand plain--installmean "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 exited1with "sign in to Xcode", which is what the reporter had already done. The expectation is now what actually reachedxcodebuild, 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 exits0. 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.
detectXcodeTeamreadIDEProvisioningTeamByIdentifierfromcom.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 anApple Developmentcertificate 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 readsIDEProvisioningTeamsand thecom.apple.dt.xcodebuilddomain (which key and which domain get written varies by Xcode version), and any provisioning profile under~/Library/Developer/Xcode/UserData/Provisioning Profilesor~/Library/MobileDevice/Provisioning Profiles, newest first. Profiles are CMS-signed but the payload plist sits in the blob as plain XML, soTeamIdentifieris scanned out of the bytes instead of shelling out tosecurity cms -Dper file. The id is the whole job, since the build already runsxcodebuild -allowProvisioningUpdatesand 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).
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.serviceWorkerin 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 aMessagePortand 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,controlleris null, andreadystays pending indefinitely. Theawaitnever 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.comsigned in and the extension never answered a single request.readynow resolves with a registration stub andpostMessageis tunneled to the background over aruntime.connectport, which is the only transport available here — an extension page embedded in a web page runs in the web content process, whereextension.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. AMessagePortcannot cross a runtime port either, so each transferred port is bridged by id and the background re-materializes a realMessagePortfrom its ownMessageChannel, then dispatches amessageevent carrying it plus thesourceandwaitUntila 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 answersget({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 realJSESSIONIDlives on.www.linkedin.com, Kondo mints a placeholder on.linkedin.comwhenever 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 aCsrf-Tokencontradicting its ownCookieheader and came back403 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.getnow re-resolves throughgetAll({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-onlygetAllfalls through to the nativeget(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.
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 servesgetURL("")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 alignedsender.originup togetURL's case for uBlock'ssender.origin === getURL("").slice(0,-1), which broke the mirror-image check: LastPass's background accepts its popup's initialize message only whennew 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 lowercasedfetch(getURL("manifest.json"))404s, and Honey and Dark Reader matchlocation.hrefagainstgetURL("/…"). 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 aschrome-extension://<uuid>/…and WebKit blocks it: "requested insecure content … must be served over HTTPS". Cloaked'scontent.csslost 18 webfonts on every injection into my.cloaked.com, which is also what buried its console.CHROME_SCHEME_REskips these and has to, because its host guard cannot separate a placeholder from a real id and a concrete-host URL may be an OAuthredirect_uriregistered with a provider; a placeholder host never is, since nothing outside the browser resolves it.walkScriptstakes 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
.appexwithcodesign -dvvand 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--teamare untouched (f24972c, tests in118c327), 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).
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
tabIdparam. 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 withreplaceState. 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 comparinglocation.hreftogetURL("/popover/popover.html"); once the query landed the compare failed, every popup RPC went out asmessages:cs, and the background'smessages:cslistener bails on any sender without asender.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?tabIdthat 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
getURLone host case, and fixsender.origininstead. Safari reports the extension UUID uppercase ingetURL,location.hrefandsender.url, and lowercase insender.origin, and bundles comparegetURL's output against both. The root args were lowercased so uBlock'ssender.origin === getURL("").slice(0,-1)would match, which broke Honey'slocation.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 atabId. Resource paths could not be lowercased at all, since Safari's resource server is case-sensitive on the host and a lowercasedfetch(getURL("manifest.json"))404s.getURLnow returns Safari's case for every argument andsender.originis aligned to it on the clone the sender fix already hands listeners, which is where mutation actually sticks. Thesender.urlhalf of the old lowercasing had already gone in43047af(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: bothtabs.onActivatedandwindows.onFocusChangedare 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'sgetSelectedTab()istabs.get(selectedTabId), so its popup's opening request made the background calltabs.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 realonActivatedstops the polling permanently so a future Safari cannot deliver everything twice (7b1b69a, hardened in61cf026), see Safari Quirks C6 and Runtime Shim. - Strip the query off a relayed
sender.urltoo. Native delivery already did. The storage relay builds its sender out oflocation.href, so an extension page whose URL carries a query reported asender.urlthat no longer equalledgetURL(path), and bundles allow-listing their own pages by exact match dropped those messages without ever callingsendResponse. 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.
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.
detectXcodeTeamread onlyIDEProvisioningTeamByIdentifier, 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 withif (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.onHistoryStateUpdatedfrom 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'spostMessagehad 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
tabson the Window returned bywindows.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 atabs.queryon the new window and synthesizes a tab when the query comes back empty, because callers indextabs[0]unconditionally and an empty array is as fatal as a missing one. It also retries withouttypewhen Safari refuses a window type it does not render (7fd15e8), see Safari Quirks C4 and Runtime Shim. -
Stop swapping the extension page's global
apiobject in the storage relay. The relay replacedchrome/browseron extension pages with a Proxy to interceptsendMessage/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. Replacingruntime.onMessagein place fails the same way; wrapping itsaddListenerdoes 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.sendMessageto the background, and only lets itssendResponseround-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.userScriptsactually 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 nocontent_scriptsat all, so its userscripts saved, listed as enabled, and never ran. The background now publishes its registry tostorage.localon every register/update/unregister, and viaduct injects a content script (only into extensions that declared the permission) that reads it atdocument_startand evaluates what matches the page. Storage rather than messaging, becauseruntime.sendMessagebroadcasts to every listener and the firstsendResponsewins: 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.jsonduring staging, because Safari doesn't load the final entry of a locale catalog.getMessagereturns""for that key in every form, and the extension then renders its raw message key. Tampermonkey's last message isv0version0("v$version$"), so its dashboard header showed a literalv0version0instead of the version whiletop_level_awaitimmediately 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}Rulescall if any single rule fails validation, so one uncompilableregexFiltercosts the extension every other rule it tried to register. Tampermonkey registers all of its*.user.jsinterception 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.postMessageto the emulated offscreen document. The shim fabricatesself.clientsfor extensions that probe for their offscreen document the Chrome way, but the fabricated client'spostMessagewas 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 transferredMessagePortand 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-
declarativeNetRequestextension as a blocking-webRequestcontent blocker. The class-level error exempted only extensions shipping staticdeclarative_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-rejectedWithHostAccesstoken back onto plaindeclarativeNetRequest, 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 mentionedonClickednext toaction/browserActionas an onClicked-driven button and injected a transparent stubdefault_popup. But an extension can registeronClickedand still wire its real popup at runtime viasetPopup, 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 setspopup/popup.htmlviasetPopupat startup and itsonClickedfires 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.backgroundRegistersActionOnClickednow scans all background files collectively and opts out when any of them sets a non-empty popup viasetPopup; an emptysetPopup({popup:""})clears the popup and doesn't opt out. The collective scan matters because viaduct prepends its own shim (which referencesonClicked+actionbut nosetPopup) tobackground.scripts, so a per-file check would short-circuit on the shim before reaching the bundle's realsetPopup. 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
keywith the lenientparseJsonc, not strictJSON.parse. A Chrome manifest carrying a BOM,//comment, or trailing comma madeJSON.parsethrow, 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. -
defaultBundleIdnow 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. -
--uninstallreads the installed app's realCFBundleIdentifierto key the native-messaging broker LaunchAgent, instead of recomputing the default id. An extension installed with a custom--bundle-idused to leave an orphanedKeepAliveagent runningopenon 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
ancestorOriginsguard 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 toancestorOrigins), 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.keyform (Up→ArrowUp,Space→" ", …) instead of the bare WebExtensions token, and falls back to theCtrl+Shift+Ydefault when a token is unmappable. A command likeCtrl+Shift+Upused to wire a shortcut that could never fire (92fa2ee), see Runtime Shim and Safari Quirks A5. - Drop the dead
try/catcharoundBuffer.from(key, "base64")inderiveChromeId— Node's base64 decoder never throws; theder.length === 0check is the real malformed-key guard (92fa2ee), see OAuth Bridge. - Strip comments before the
importScriptsdynamic-argument residue test, soimportScripts("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.onMessagelisteners 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__vHotkeyWiredflag derived fromchrome.runtime.getManifest().content_scripts. Safari stripscontent_scriptsfromgetManifest()inside a content script, so the flag read false there, the capture never installed, and the hotkey fired into an empty list. SuperDev Pro'sCmd+Shift+Ssidebar toggle did nothing as a result (itsonClickedandcommands.onCommandare both dead in Safari, so the hotkey is the only path). Dropped thegetManifest-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;writePolyfillcopied viaduct's webextension-polyfill build over that exact file, so uBlock'svapi.js(compiled against its own copy) threw at load and every content script after it, includingcontentscript.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 underviaduct-browser-polyfill.min.jswhen 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://) frompermissions/optional_permissionsduring transform. Tampermonkey shipschrome://favicon/inpermissions; 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-ignoredhttps://pattern but fatal for a Chrome-only scheme Safari can't grant anywhere. The transform now drops these the same wayhost_permissionsalready filters unparseable schemes, and the analyzer reports the case as auto-fixed (31d13b9), see Manifest Transform and Safari Quirks A6. Closes #11.
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
onClickedactions and preserveversion_namein the transformed manifest (5b1bd03), see Manifest Transform. - Guard
ancestorOriginsreads and rewrite self-pagechrome-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://fromcontextMenusURL patterns (4a5cae9), see Safari Quirks F4. - Cap
commandsat Safari's 4-shortcut limit (d3fcb9a), see Manifest Transform / Safari Quirks A4. - Make top-level content-script globals re-injection-safe (
const/let→var) so Safari's double-evaluation of adocument_end/all_framesgroup into a shared world can't throw "Can't create duplicate variable" (1f22a10), then normalize./and backslash content-scriptjspaths so every referenced file is reached (2dc30ca), see Safari Quirks E4. Closes #4. - Proxied XHR fires
timeout(+loadend), noterror, on a timeout (2501a8b), see Safari Quirks D6. Closes #6.
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.connectNativeworks 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.
Theme: main-world content scripts.
- Re-declare content-script page-world injections as
world: MAINso scripts that expect the page's own JS context behave on Safari (d35e3ad), see Manifest Transform and Safari Quirks.
Theme: popup-less action buttons.
- Popover-free toggle for popup-less
onClickedactions, 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-
webRequestcontent 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. -
deriveAppNamewhitelists characters to stop XML/scheme injection into generated project files (ba89cc7).
Theme: URL-rewrite correctness fix.
- Keep concrete-host
chrome-extension://URLs intact rather than rewriting them (1b7b6f5), see Safari Quirks.
Theme: docs.
- Spell out the Safari website-access grant step users must perform after install (
2ea6657), see Build and Install.
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?tabIdquery from popupsender.url(49c41cb), see Runtime Shim. - Keep a
declarativeContent-gated action button clickable and further tighten popup sizing (db06848), see Safari Quirks.
Theme: docs / release chore.
- Smaller viaduct ASCII icon in the README (
39820ba); release bump (886284c). No code changes.
Theme: docs / release chore.
- New viaduct ASCII icon and README banner update (
357a49e); release bump (a62f069). No code changes.
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
--verifymode (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.sessioncorrectness, missing runtime APIs (f344d7a); external-messaging events and proxy-config substitution guard (54172b4). - Resolve
__MSG_*__i18n placeholders in the generatedbackground.htmltitle (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 scratchscripts/anddocs/dirs (7f971ad); new README ASCII icon (dd17de2).
Theme: sidePanel + action badge fixes.
- Safari
sidePaneltoggle 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).
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.idto the UUID host and strip theruntime.idprefix 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 inb05abd8. - Supporting shim fixes for the same goal: route popup port by lowercasing
sender.urlhost (e90a45f); forward cloned-port methods bound to the real port for Safari's brand check (7c39c87);defineProperty-wrap nativeonConnect/onMessage/cookiesevents (16eccc6); backfillstorage.session.setAccessLeveland clearruntime.lastErrorso background init can't hang or reject (e0a70ea,f3eb880); carry the httpOnly session cookie through the native-host auth proxy (6071347); guardcookies.onChangedagainst 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-
chromeglobals; backfill MV3 enums/storage/userScriptsevents on the resolved namespace (73c1926,ca523e8,b81dda0,1880790); blockingwebRequestdegrades 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-parenimportScriptsneutralization + DNR path-escape guard (e8830e0). - Large refactor splitting
src/into concern foldersinput/manifest/analyze/runtime/build(a07abde), extracting the shim runtime JS tosrc/runtime/safari-compat-shim.js(2927cdf) and the Safari compat-data tables out ofmanifest.ts(3f919d0); addedCLAUDE.mdarchitecture 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).
Theme: UA-sniffing detection + a bug hunt.
- Detect
navigator.userAgentChrome-version sniffing so the analyzer can warn about it (a534d69), see Analyzer. - Surface shim-emulated APIs in analysis output and fix
pluginkitparsing (30509f8), see Analyzer. - Bug-hunt fixes: 3 conversion bugs (
cef6aee); bundle-id collisions + a symlink leak in staging (b0c81a1); native-proxyResponsecrashes on out-of-range/null-body status (3a16e42); shimfill()no longer clobbers legitimately-falsy native members (273272b); sanitize hosts/origin in generated native-proxy Swift literals (dab04f1); fix report handling of the MV2page_actionfold (72158b8).
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/shortcutspage (503e14f), and warn at build time on hardcodedchrome://extensions/shortcuts(orsettings) links (f74597a); documented in the shim feature list (e7d1653), see Safari Quirks. - First real test suite lands and hardens the release:
applyDnr(7e8477d),analyzeManifestCSP/host-misplacement (1156251),scanExtensiondetections (2cc6c34),extract.tsCRX3/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 in2d7b4ac).
(This tag range also sweeps up the tail of the README ASCII-banner iteration 700c321→85e408c and the final publishing-pipeline commits 1d728d2→47b3dea, see the v1.2.x note below.)
Theme: the rename to viaduct, @magicelk235 scoping, and the Trusted-Publishing burst.
This is where chrome2safari became viaduct and npm publishing was rebuilt:
- Rename
chrome2safari→viaductacross README, package, CLI, and source (6624163,c497683); scope the package as@magicelk235/viaductand add a release publish workflow (aaa1437); switch to npm Trusted Publishing (OIDC) instead ofNPM_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 onmacos-latest, the package isos:darwin),cca58cd(shell-expanded test glob),1d728d2(pin node, verify npm version),01cc7a7/8073d4e/0dadd91(registry-url and.npmrctoken-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/appplus a CORS proxy and build-config hardening (8255ebc); first test suite + CI gate with corpus-run bug fixes (0dd9519);oauth-bridgeidempotency, 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); completechrome.i18nand 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-workerimportScriptshoisted into the background page for a CSP-safe fix (fc49074).
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).
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-polyfillforbrowser.*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.
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-.appexbundle-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 pinOriginforapi.anthropic.com(5d1d542); a second runtime shim pass with achrome.debuggerpolyfill 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.onDetachfixes (146b6ca). - Made the package npm/npx-publishable (
3013af2) and cut the release (c996214).
Viaduct CLI · @magicelk235/viaduct · PolyForm Shield 1.0.0 · Verified against src/ and grounded in git history.