Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eca524a
feat(widget-cdn): add Azure Blob sandbox deployment
babkenmes Jul 29, 2026
52c6b0c
feat(widget): add shared protocol versioning
babkenmes Jul 29, 2026
9027abc
Merge branch 'dev' into codex/widget-cdn-azure-sandbox
babkenmes Aug 3, 2026
e36b711
fix: adjust workspace specifiers for widget types in pnpm-lock.yaml
babkenmes Aug 3, 2026
530a845
fix: update ignoreCommand to include pnpm-lock.yaml and pnpm-workspac…
babkenmes Aug 3, 2026
638b083
fix: update ignoreCommand in vercel.json to exclude pnpm lock files
babkenmes Aug 3, 2026
bc07a50
fix: update widget manifest handling and verification logic across co…
babkenmes Aug 4, 2026
780aa01
ci: expose Azure sandbox manifest public key
babkenmes Aug 4, 2026
b7278e3
test: expect Azure widget manifest URL
babkenmes Aug 4, 2026
1b3928e
fix: update MANIFEST_VERIFY_PUBLIC_KEY_SPKI_B64 to the correct value
babkenmes Aug 4, 2026
75d9faf
fix: use stable manifest signing key for Azure
babkenmes Aug 4, 2026
3c0d747
feat: expose Deposit Widget through CDN remote and loader packages
babkenmes Aug 4, 2026
fe62121
feat(widget-cdn): approvable cleanup of old builds, Azure-only delive…
babkenmes Aug 4, 2026
d9fc0e9
chore(widget-cdn): trigger deploy to verify smoke test after removing…
babkenmes Aug 4, 2026
7abe860
fix(widget-evm): sync Layerswap chains into host-supplied wagmi config
babkenmes Aug 4, 2026
b71fb79
fix(widget-evm): bypass stale connector chain snapshot when switching…
babkenmes Aug 4, 2026
85d9916
fix(wallet-evm): reset RPC health verdict on chain switch
babkenmes Aug 5, 2026
69d1960
fix(widget-cdn): release mount slot on throwing initial render and co…
babkenmes Aug 5, 2026
f5cda54
ci(widget-cdn): trigger push deploys from dev with path filters
babkenmes Aug 5, 2026
54247ec
perf(widget-core): lazy-load SwapDetails in the Deposit processing step
babkenmes Aug 5, 2026
2fffaec
chore(widget): block npm publish while the loader points at the sandb…
babkenmes Aug 5, 2026
f858897
fix(wallet-evm): invalidate in-flight RPC probes by generation, not c…
babkenmes Aug 5, 2026
a5f0c20
docs(widget-example): hoist widget props to stable references in the …
babkenmes Aug 5, 2026
5aaf633
ci(widget): secure CDN publishing workflow
babkenmes Aug 26, 2026
b78fe2a
Merge branch 'dev' into codex/widget-cdn-azure-sandbox
babkenmes Aug 26, 2026
6959b95
fix(widget-types): emit .js extensions in ESM output for Node compati…
babkenmes Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
}
6 changes: 6 additions & 0 deletions .changeset/secure-widget-cdn-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@layerswap/widget-js': patch
'@layerswap/widget-react': patch
---

Rotate the widget manifest trust anchor and prepare the public loaders for the protected CDN release workflow.
8 changes: 8 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Frontend release security boundary. Changes to these files require review
# from a maintainer who can approve widget publishing and deployment changes.
/.github/CODEOWNERS @layerswap/frontend-release-maintainers
/.github/workflows/ @layerswap/frontend-release-maintainers
/.github/scripts/ @layerswap/frontend-release-maintainers
/apps/widget-cdn/scripts/ @layerswap/frontend-release-maintainers
/packages/widget/js/src/manifest.ts @layerswap/frontend-release-maintainers
/pnpm-lock.yaml @layerswap/frontend-release-maintainers
57 changes: 57 additions & 0 deletions .github/scripts/resolve-widget-cdn-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Resolve the widget-cdn Azure target for a GitHub environment.
#
# Inputs (env):
# DEPLOY_TARGET GitHub environment name (required)
# RAW_AZURE_CLIENT_ID \
# RAW_AZURE_TENANT_ID | the environment's `vars.*`, passed through by
# RAW_AZURE_SUBSCRIPTION_ID | the workflow (may be empty)
# RAW_AZURE_STORAGE_ACCOUNT |
# RAW_AZURE_STORAGE_CONTAINER
# RAW_CDN_BASE_URL /
#
# For widget-cdn-azure-sandbox, missing values fall back to the personal
# sandbox identifiers (they are identifiers, not credentials) so the sandbox
# works before its environment variables exist. Any other environment must be
# fully configured in Settings → Environments, or this fails with a clear
# error. Resolved values are appended to $GITHUB_ENV.
set -euo pipefail

TARGET="${DEPLOY_TARGET:?DEPLOY_TARGET is required}"
CLIENT_ID="${RAW_AZURE_CLIENT_ID:-}"
TENANT_ID="${RAW_AZURE_TENANT_ID:-}"
SUBSCRIPTION_ID="${RAW_AZURE_SUBSCRIPTION_ID:-}"
STORAGE_ACCOUNT="${RAW_AZURE_STORAGE_ACCOUNT:-}"
CONTAINER="${RAW_AZURE_STORAGE_CONTAINER:-}"
BASE_URL="${RAW_CDN_BASE_URL:-}"

if [ "$TARGET" = "widget-cdn-azure-sandbox" ]; then
CLIENT_ID="${CLIENT_ID:-69ca1ebc-8381-4b6d-864a-497053bdd827}"
TENANT_ID="${TENANT_ID:-8e8e42bf-64ee-4057-99ab-8288126d4ebb}"
SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-f60908b5-2c18-438c-89c5-510a01d2a802}"
STORAGE_ACCOUNT="${STORAGE_ACCOUNT:-layerswapcdntest}"
else
MISSING=""
[ -z "$CLIENT_ID" ] && MISSING="$MISSING AZURE_CLIENT_ID"
[ -z "$TENANT_ID" ] && MISSING="$MISSING AZURE_TENANT_ID"
[ -z "$SUBSCRIPTION_ID" ] && MISSING="$MISSING AZURE_SUBSCRIPTION_ID"
[ -z "$STORAGE_ACCOUNT" ] && MISSING="$MISSING AZURE_STORAGE_ACCOUNT"
if [ -n "$MISSING" ]; then
echo "::error::Environment '$TARGET' is missing variables:$MISSING — configure them in Settings → Environments."
exit 1
fi
fi

CONTAINER="${CONTAINER:-widget-cdn}"
BASE_URL="${BASE_URL:-https://${STORAGE_ACCOUNT}.blob.core.windows.net/${CONTAINER}}"

{
echo "AZURE_CLIENT_ID=$CLIENT_ID"
echo "AZURE_TENANT_ID=$TENANT_ID"
echo "AZURE_SUBSCRIPTION_ID=$SUBSCRIPTION_ID"
echo "AZURE_STORAGE_ACCOUNT=$STORAGE_ACCOUNT"
echo "AZURE_STORAGE_CONTAINER=$CONTAINER"
echo "CDN_BASE_URL=${BASE_URL%/}"
} >> "$GITHUB_ENV"

echo "Resolved target $TARGET (account: $STORAGE_ACCOUNT, container: $CONTAINER)"
14 changes: 9 additions & 5 deletions .github/workflows/rebase-main-sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ on:
push:
branches: [main]
permissions:
contents: write
contents: read
jobs:
rebase-main-sandbox:
permissions:
contents: write
timeout-minutes: 2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Set Git config
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Merge main to main-sandbox
run: |
git fetch --unshallow
git checkout main-sandbox
git fetch origin main-sandbox
git checkout -B main-sandbox origin/main-sandbox
git rebase main
git push
git push origin HEAD:main-sandbox
202 changes: 202 additions & 0 deletions .github/workflows/widget-cdn-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
name: widget-cdn — cleanup old builds

# Reference-aware retention for the widget CDN storage in Azure Blob Storage
# (policy in apps/widget-cdn/scripts/cleanup-lib.mjs), run as
# plan → approve → apply:
#
# 1. The `plan` job computes what is deletable, prints the keep/delete
# report to the run summary, and uploads the plan as an artifact. It also
# warns when the promoted build's manifest is near expiry (the live
# channel fails closed past it).
# 2. The `apply` job waits on the target ENVIRONMENT's required reviewers —
# the "we can delete these 4 old builds, delete them?" button. Review the
# plan job's summary, then approve. It deletes exactly the approved plan
# (intersected with fresh state, so anything promoted or uploaded since
# planning stays protected). If the plan is empty, apply is skipped and
# no approval is requested.
#
# The target is a GitHub environment carrying the Azure identity as variables
# (see .github/scripts/resolve-widget-cdn-env.sh; the sandbox has inline
# fallbacks, production must be configured in Settings → Environments). Both
# jobs declare the environment — if it has required reviewers, the read-only
# plan needs one approval and the delete a second, informed one.
#
# Note: `schedule` only fires from the repository's default branch (`dev`).

on:
schedule:
- cron: "17 4 * * 1" # weekly sandbox plan; deletes only if approved
workflow_dispatch:
inputs:
environment:
description: "Storage target (GitHub environment)"
type: choice
options: [widget-cdn-azure-sandbox, widget-cdn-production]
default: widget-cdn-azure-sandbox
keep_recent:
description: "Most recent builds kept per channel regardless of expiry"
type: string
required: false
default: "3"
asset_grace_days:
description: "Grace period before unreferenced shared assets are deleted"
type: string
required: false
default: "2"

concurrency:
group: widget-cdn-cleanup-${{ inputs.environment || 'widget-cdn-azure-sandbox' }}
cancel-in-progress: false

permissions:
contents: read

env:
DEPLOY_TARGET: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }}
KEEP_RECENT: ${{ inputs.keep_recent || '3' }}
ASSET_GRACE_DAYS: ${{ inputs.asset_grace_days || '2' }}

jobs:
plan:
runs-on: ubuntu-latest
timeout-minutes: 20
environment: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }}
permissions:
contents: read
id-token: write
outputs:
has-deletions: ${{ steps.plan.outputs.has-deletions }}

steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false

- name: Resolve storage target
env:
RAW_AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
RAW_AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
RAW_AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
RAW_AZURE_STORAGE_ACCOUNT: ${{ vars.AZURE_STORAGE_ACCOUNT }}
RAW_AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }}
RAW_CDN_BASE_URL: ${{ vars.CDN_BASE_URL }}
run: bash .github/scripts/resolve-widget-cdn-env.sh

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6

- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build shared widget protocol
run: pnpm --filter @layerswap/widget-types build

- name: Log in to Azure with OIDC
uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}

- name: Compute cleanup plan (dry run)
id: plan
working-directory: apps/widget-cdn
run: |
set -euo pipefail
node scripts/cleanup-azure.mjs \
--keep-recent "$KEEP_RECENT" --asset-grace-days "$ASSET_GRACE_DAYS" \
--plan-out "$RUNNER_TEMP/plan.json" 2>&1 | tee "$RUNNER_TEMP/cleanup.log"
COUNT=$(node -e "const p=require(process.env.RUNNER_TEMP+'/plan.json'); console.log(p.deleteBuilds.length + p.deleteAssets.length)")
echo "has-deletions=$([ "$COUNT" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
{
echo "## widget-cdn cleanup plan (${DEPLOY_TARGET})"
echo ""
if [ "$COUNT" -gt 0 ]; then
echo "**$COUNT object group(s) are deletable. Approve the \`apply\` job to delete them.**"
else
echo "Nothing to delete — apply will be skipped."
fi
echo ""
echo '```'
cat "$RUNNER_TEMP/cleanup.log"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload plan artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: cleanup-plan
path: ${{ runner.temp }}/plan.json
retention-days: 7

apply:
needs: plan
if: ${{ needs.plan.outputs.has-deletions == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
# The approval gate: this environment's required reviewers must approve
# before anything is deleted from the container.
environment: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }}
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false

- name: Resolve storage target
env:
RAW_AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
RAW_AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
RAW_AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
RAW_AZURE_STORAGE_ACCOUNT: ${{ vars.AZURE_STORAGE_ACCOUNT }}
RAW_AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }}
RAW_CDN_BASE_URL: ${{ vars.CDN_BASE_URL }}
run: bash .github/scripts/resolve-widget-cdn-env.sh

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6

- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build shared widget protocol
run: pnpm --filter @layerswap/widget-types build

- name: Download approved plan
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: cleanup-plan
path: ${{ runner.temp }}

- name: Log in to Azure with OIDC
uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}

- name: Apply approved plan
working-directory: apps/widget-cdn
run: |
set -euo pipefail
node scripts/cleanup-azure.mjs --apply --plan-in "$RUNNER_TEMP/plan.json" \
--keep-recent "$KEEP_RECENT" --asset-grace-days "$ASSET_GRACE_DAYS" \
2>&1 | tee "$RUNNER_TEMP/cleanup.log"
{
echo "## widget-cdn cleanup applied (${DEPLOY_TARGET})"
echo ""
echo '```'
cat "$RUNNER_TEMP/cleanup.log"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
Loading