Skip to content

ci(widget): secure CDN publishing workflow - #2107

Merged
babkenmes merged 26 commits into
devfrom
codex/widget-cdn-azure-sandbox
Aug 26, 2026
Merged

ci(widget): secure CDN publishing workflow#2107
babkenmes merged 26 commits into
devfrom
codex/widget-cdn-azure-sandbox

Conversation

@babkenmes

@babkenmes babkenmes commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator
  • add isolated, read-only widget PR validation
  • build unsigned release candidates before environment approval
  • deploy sandbox automatically after merge to dev
  • restrict production deployment to manual dispatch from main
  • scope signing secrets and Azure OIDC to protected deployment jobs
  • pin GitHub Actions to immutable commit SHAs

babkenmes and others added 17 commits July 29, 2026 16:24
- @layerswap/widget-types: shared DepositWidgetProps/DepositConfig contract
- widget-cdn: new ./DepositWidget and ./mountDeposit MF exposes; shared
  useWalletProviders hook and mountRoot single-live-mount guard
- @layerswap/widget-js: mountDepositWidget + generic WidgetHandle
- @layerswap/widget-react: LayerswapDepositWidget via shared RemoteWidgetHost
- bundle budget is now enforced per expose (a page mounts exactly one widget)
- widget-react-host example: Swap/Deposit tab toggle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry, environment-parameterized deploys

Cleanup mechanism (plan → approve → apply):
- scripts/cleanup-lib.mjs: reference-aware retention planner — keeps
  promoted, unexpired, N-most-recent, and grace-period builds; mark-and-
  sweeps the shared /assets/ namespace against retained manifests (never
  age-only — chunks are shared across builds); deletes manifest-first and
  aborts if a channel pointer changes between planning and deletion
- scripts/cleanup-azure.mjs: CLI (dry run by default, --apply to delete,
  --plan-out/--plan-in for the human-approval round-trip: apply deletes
  exactly the reviewed plan intersected with fresh state)
- azure-lib.mjs: paginated listObjects (Blob REST) + idempotent deleteObject
- widget-cdn-cleanup.yml: plan job posts the keep/delete report and uploads
  the plan; apply waits on environment required-reviewer approval and is
  skipped when nothing is deletable; weekly schedule doubles as the
  channel-expiry watchdog (warns <7 days before the live manifest expires)

Azure is the only CDN path — remove Cloudflare R2/Worker:
- delete deploy-r2/rollback-r2/r2-lib/cleanup-r2, worker/, the R2 deploy
  workflow, and @aws-sdk/client-s3 + wrangler + workers-types deps
- loader docs/CSP examples now reference the Azure Blob origin

Environment-parameterized deploys (production is drop-in):
- widget-cdn-azure-sandbox.yml → widget-cdn-deploy.yml with an environment
  input; Azure identity comes from environment variables and the signing
  key from the environment secret (one keypair per environment)
- .github/scripts/resolve-widget-cdn-env.sh: sandbox identifiers as inline
  fallbacks so the sandbox works before any environment is configured;
  production fails with a clear missing-variables error until an admin
  fills in widget-cdn-production; production deploys refused off the
  default branch; deploys pause on environment approval once required
  reviewers are configured

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… stale CDN_BASE_URL variable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a host passes an external wagmiConfig, the widget adopted it as-is,
so any chain the host didn't declare (hosts typically register only
mainnet) was missing from config.chains — wagmi connectors then throw
ChainNotConfiguredError on switchChain before ever prompting the wallet,
surfacing as "Network switch failed" with no detail.

Append Layerswap EVM chains and transports to the adopted config via
wagmi's _internal.chains store (host order and transports win), and
include the underlying error's shortMessage in the network-switch
failure message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… on host configs

wagmi hands each connector a snapshot of config.chains at setup time
(connectorFn({ chains: chains.getState(), ... })). Host apps create their
connectors at page load, before syncLayerswapChains appends the Layerswap
chains to the adopted config, so connector.switchChain still validated
against the stale snapshot and threw ChainNotConfiguredError for any chain
the host didn't declare — appending chains to the live store was not enough.

For adopted external configs, drive the connector's EIP-1193 provider
directly (wallet_switchEthereumChain, wallet_addEthereumChain on 4902, then
switch again), mirroring what wagmi's injected connector does after its
snapshot check. wagmi state still updates via the chainChanged event, and
the subsequent getWalletClient call resolves the chain from the live
config.chains getter, which the sync fix populates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The health store only re-probed on connector/connectedness changes, so
after a chain switch the stale unhealthy verdict from the previous chain
flashed until the banner's 1.5s poll re-checked the new chain. Reset the
snapshot to unknown on chain/connector change (including resubscribe
catch-up) and discard in-flight probe results that resolve after the
wallet moved to a different chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
layerswap-embed-example Ready Ready Preview Aug 26, 2026 4:16pm
layerswapapp Ready Ready Preview Aug 26, 2026 4:16pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
layerswap-explorer Ignored Ignored Preview Aug 26, 2026 4:16pm

Request Review

babkenmes and others added 6 commits August 5, 2026 19:28
…ntain widget crashes

A throwing first render in mountRoot() left liveMounts incremented with no
handle to destroy, permanently locking out every future mount()/mountDeposit()
on the page. Roll the counter back and unmount the root before rethrowing.

Also wrap both the initial and update() renders in a MountErrorBoundary so the
imperative mount path gets the same render-crash containment the React loader
path already has via WidgetErrorBoundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The push trigger only watched the feature branch, so deployment would have
silently stopped once the branch merged into dev. Trigger on dev (keeping the
feature branch until the PR lands), filtered to the paths that actually feed
the widget build so unrelated monorepo pushes don't deploy.

Approval comes from required reviewers on the widget-cdn-azure-sandbox
environment (repo Settings), which the job already targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SwapDetails transitively pulls the whole Swap-processing subtree into the
Deposit exposes' sync bundle, leaving them at 99.3% of the CDN bundle-budget
gate. Load it lazily behind Suspense, mirroring FormWrapper's existing
lazy-load of the same component.

Deposit exposes drop from 659.5 to 633.7 KiB gzip (budget 664.1 KiB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ox CDN

WIDGET_MANIFEST_URL is deliberately hardcoded, which means the only defense
against publishing @layerswap/widget-js/-react with the sandbox storage
account baked in was memory. Add a prepublishOnly gate to both packages that
hard-fails while the loader source still references layerswapcdntest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ontext identity

Comparing connector/chain ids can't tell two in-flight probes for the same
chain apart: on a fast A->B->A flip-flop, the original A-probe resolving late
would overwrite the fresh verdict. Replace the identity check with a monotonic
generation counter, bumped on every connector/chain/connectedness change and
whenever a newer probe starts, so only the latest probe's verdict lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…host example

Inline object/function literals defeated useWalletProviders' reference-
equality memo, rebuilding wallet providers on every host re-render — and the
example is the pattern integrators copy. Hoist config, walletProvidersConfig,
destination, callbacks, and handlers to typed module constants with a comment
explaining the stability requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arentant
arentant previously approved these changes Aug 26, 2026
…bility

The widget-cdn release-tooling tests run the built widget-types package
directly under node --test, where extensionless relative imports fail to
resolve. Enable tsc-alias resolveFullPaths (same pattern as wallets/core)
so the build appends .js extensions to relative imports in dist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@babkenmes
babkenmes merged commit c20e1df into dev Aug 26, 2026
5 checks passed
@babkenmes
babkenmes deleted the codex/widget-cdn-azure-sandbox branch August 26, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants