mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-20 02:41:01 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51eecf9e08 | |||
| 78e93e9766 | |||
| 83d66feacc | |||
| d6ace14e70 | |||
| 2cbfd21539 | |||
| f7c070e45b | |||
| 06ed9734c6 | |||
| d93d4c6cd0 | |||
| faba2dee5d | |||
| ca3976fe62 |
@@ -0,0 +1,41 @@
|
|||||||
|
# pwap/web Docker build context (root) — exclude everything not needed
|
||||||
|
# for `web/` build. Other monorepo subprojects stay out of the image.
|
||||||
|
|
||||||
|
# Other monorepo dirs (we only need web/ + shared/)
|
||||||
|
exchange/
|
||||||
|
mobile/
|
||||||
|
pwap-mobile/
|
||||||
|
docs/
|
||||||
|
res/
|
||||||
|
|
||||||
|
# All node_modules everywhere
|
||||||
|
**/node_modules/
|
||||||
|
**/dist/
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Git, GitHub
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Env files (built-in vars are passed as build-args from CI)
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
|
# Editor / OS
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
**/.eslintcache
|
||||||
|
**/coverage/
|
||||||
|
|
||||||
|
# Already-built artifacts (we rebuild fresh inside container)
|
||||||
|
web/dist/
|
||||||
|
shared/**/dist/
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: CodeQL (SAST)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
schedule:
|
||||||
|
# Every Sunday at 02:00 UTC — catches CVEs disclosed during the week
|
||||||
|
- cron: '0 2 * * 0'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: codeql-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze ${{ matrix.language }}
|
||||||
|
runs-on: pwap-runner
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [javascript-typescript]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# OWASP top-10 + injection + auth flaws + prototype pollution
|
||||||
|
queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Perform analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: /language:${{ matrix.language }}
|
||||||
|
# GitHub Advanced Security dashboard upload requires paid plan.
|
||||||
|
# SARIF saved as a downloadable artifact instead.
|
||||||
|
upload: false
|
||||||
|
output: /tmp/codeql-results
|
||||||
|
|
||||||
|
- name: Upload SARIF as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: codeql-sarif-${{ matrix.language }}
|
||||||
|
path: /tmp/codeql-results/*.sarif
|
||||||
|
retention-days: 7
|
||||||
@@ -6,14 +6,25 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
rollback_to:
|
||||||
|
description: 'Rollback to git SHA (skips build, redeploys old image). Empty = normal deploy.'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # version bump commit
|
||||||
|
packages: write # GHCR push
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
||||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: pezkuwichain/pwap-web
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ========================================
|
# ========================================
|
||||||
@@ -21,7 +32,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
web:
|
web:
|
||||||
name: Web App
|
name: Web App
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -74,14 +85,163 @@ jobs:
|
|||||||
name: web-dist
|
name: web-dist
|
||||||
path: web/dist/
|
path: web/dist/
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# BUILD & PUSH DOCKER IMAGE TO GHCR
|
||||||
|
# Immutable artifact for audit + rollback (vs ephemeral GHA artifact).
|
||||||
|
# Tagged with git SHA so any commit can be redeployed by SHA.
|
||||||
|
# ========================================
|
||||||
|
build-image:
|
||||||
|
name: Build & Push Image
|
||||||
|
runs-on: pwap-runner
|
||||||
|
needs: [web, telegram-gate]
|
||||||
|
if: |
|
||||||
|
github.ref == 'refs/heads/main' &&
|
||||||
|
(github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write # cosign keyless signing via Sigstore OIDC
|
||||||
|
outputs:
|
||||||
|
image_sha: ${{ steps.meta.outputs.image_sha }}
|
||||||
|
image_digest: ${{ steps.build.outputs.digest }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||||
|
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "image_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./web/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}
|
||||||
|
${{ steps.meta.outputs.image }}:latest
|
||||||
|
build-args: |
|
||||||
|
VITE_NETWORK=MAINNET
|
||||||
|
VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||||
|
VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||||
|
VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||||
|
VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||||
|
VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:cache
|
||||||
|
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
|
||||||
|
provenance: false
|
||||||
|
|
||||||
|
- name: Sign image with cosign (keyless, Sigstore Fulcio)
|
||||||
|
env:
|
||||||
|
COSIGN_EXPERIMENTAL: '1'
|
||||||
|
run: |
|
||||||
|
IMAGE_DIGEST="${{ steps.meta.outputs.image }}@${{ steps.build.outputs.digest }}"
|
||||||
|
# cosign needs its own registry auth — docker/login-action only writes
|
||||||
|
# ~/.docker/config.json which cosign on self-hosted runner can't read
|
||||||
|
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||||
|
echo "Signing $IMAGE_DIGEST"
|
||||||
|
cosign sign --yes "$IMAGE_DIGEST"
|
||||||
|
echo "✅ Image signed (transparency log: rekor.sigstore.dev)"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# TELEGRAM CEO APPROVAL GATE
|
||||||
|
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
|
||||||
|
# writes the gate file to /tmp/pexsec-gates/<sha> when CEO clicks
|
||||||
|
# Approve/Cancel in Telegram. 30-minute timeout = deploy cancelled.
|
||||||
|
# ========================================
|
||||||
|
telegram-gate:
|
||||||
|
name: Telegram deploy approval
|
||||||
|
runs-on: pwap-runner
|
||||||
|
needs: [web, security-audit]
|
||||||
|
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Send approval request and wait
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
|
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
MESSAGE: ${{ github.event.head_commit.message }}
|
||||||
|
run: |
|
||||||
|
SHORT="${SHA:0:7}"
|
||||||
|
GATE_DIR="/tmp/pexsec-gates"
|
||||||
|
mkdir -p "$GATE_DIR" 2>/dev/null || true
|
||||||
|
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Strip Markdown special chars to prevent Telegram parse errors
|
||||||
|
SAFE_MSG=$(echo "${MESSAGE}" | head -1 | tr -d '_*`[]()#|{}!' | cut -c1-120)
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"chat_id\": \"${CEO_CHAT_ID}\",
|
||||||
|
\"parse_mode\": \"Markdown\",
|
||||||
|
\"text\": \"🚀 *pwap/web Deploy Approval*\\n\\n\`${SHORT}\` — ${ACTOR}\\n\\n_${SAFE_MSG}_\\n\\nTargets: app.pezkuwichain.io + pex.mom\",
|
||||||
|
\"reply_markup\": {
|
||||||
|
\"inline_keyboard\": [[
|
||||||
|
{\"text\": \"✅ Approve\", \"callback_data\": \"deploy_approve:${SHORT}\"},
|
||||||
|
{\"text\": \"❌ Cancel\", \"callback_data\": \"deploy_cancel:${SHORT}\"}
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|
||||||
|
echo "Waiting for Telegram approval (max 30 min)..."
|
||||||
|
TIMEOUT=1800
|
||||||
|
ELAPSED=0
|
||||||
|
while [ $ELAPSED -lt $TIMEOUT ]; do
|
||||||
|
if [ -f "$GATE_DIR/$SHORT" ]; then
|
||||||
|
DECISION=$(cat "$GATE_DIR/$SHORT")
|
||||||
|
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||||
|
if [ "$DECISION" = "approved" ]; then
|
||||||
|
echo "Deploy approved."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Deploy cancelled."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
ELAPSED=$((ELAPSED + 10))
|
||||||
|
done
|
||||||
|
echo "No approval received within 30 minutes — deploy cancelled."
|
||||||
|
exit 1
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
|
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
|
||||||
# ========================================
|
# ========================================
|
||||||
bump-version:
|
bump-version:
|
||||||
name: Bump Version
|
name: Bump Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web, security-audit]
|
needs: [web, security-audit, telegram-gate, build-image]
|
||||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
# Skip on rollback (workflow_dispatch with rollback_to set)
|
||||||
|
if: |
|
||||||
|
github.ref == 'refs/heads/main' &&
|
||||||
|
(github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||||
outputs:
|
outputs:
|
||||||
new_version: ${{ steps.bump.outputs.version }}
|
new_version: ${{ steps.bump.outputs.version }}
|
||||||
|
|
||||||
@@ -116,19 +276,80 @@ jobs:
|
|||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
|
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
|
||||||
|
# Pulls SHA-tagged image from GHCR, extracts /dist, scp to VPS.
|
||||||
|
# Health check + auto-rollback to .deploy-tag-prev on failure.
|
||||||
# ========================================
|
# ========================================
|
||||||
deploy-app:
|
deploy-app:
|
||||||
name: Deploy app.pezkuwichain.io
|
name: Deploy app.pezkuwichain.io
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [bump-version]
|
needs: [telegram-gate, bump-version, build-image]
|
||||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.telegram-gate.result == 'success' &&
|
||||||
|
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
env:
|
||||||
|
DOMAIN: app.pezkuwichain.io
|
||||||
|
TARGET_PATH: /var/www/subdomains/app
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Determine image SHA
|
||||||
uses: actions/download-artifact@v4
|
id: sha
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||||
|
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Rolling back to: ${{ github.event.inputs.rollback_to }}"
|
||||||
|
else
|
||||||
|
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Capture currently-live SHA (for auto-rollback)
|
||||||
|
id: prev
|
||||||
|
run: |
|
||||||
|
# /.deploy-sha is written into every deploy; read what's live now
|
||||||
|
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||||
|
echo "Previous live SHA: ${PREV:-unknown}"
|
||||||
|
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
name: web-dist
|
registry: ${{ env.REGISTRY }}
|
||||||
path: dist/
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install cosign (for verify)
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
- name: Verify image signature (cosign keyless)
|
||||||
|
env:
|
||||||
|
COSIGN_EXPERIMENTAL: '1'
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||||
|
echo "Verifying signature for $IMAGE"
|
||||||
|
cosign verify "$IMAGE" \
|
||||||
|
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||||
|
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||||
|
> /dev/null
|
||||||
|
echo "✅ Signature valid — image was built by trusted CI"
|
||||||
|
|
||||||
|
- name: Extract /dist from image
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
mkdir -p dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
# Stamp this build's SHA into dist so future deploys can read PREV
|
||||||
|
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||||
|
ls -la dist/ | head -10
|
||||||
|
|
||||||
- name: Deploy to DEV VPS
|
- name: Deploy to DEV VPS
|
||||||
uses: appleboy/scp-action@v1.0.0
|
uses: appleboy/scp-action@v1.0.0
|
||||||
@@ -141,25 +362,158 @@ jobs:
|
|||||||
target: '/var/www/subdomains/app'
|
target: '/var/www/subdomains/app'
|
||||||
strip_components: 1
|
strip_components: 1
|
||||||
|
|
||||||
- name: Post-deploy notification
|
- name: Health check (60s window)
|
||||||
|
id: healthcheck
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Deployed v${{ needs.bump-version.outputs.new_version }} to app.pezkuwichain.io"
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# ── Automatic rollback: pull PREV SHA image, redeploy, recheck ──
|
||||||
|
- name: Auto-rollback to previous SHA
|
||||||
|
id: rollback
|
||||||
|
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||||
|
run: |
|
||||||
|
PREV="${{ steps.prev.outputs.prev }}"
|
||||||
|
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
rm -rf dist && mkdir dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
echo "$PREV" > dist/.deploy-sha
|
||||||
|
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: SCP rollback artifact
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
uses: appleboy/scp-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
|
||||||
|
source: 'dist/*'
|
||||||
|
target: '/var/www/subdomains/app'
|
||||||
|
strip_components: 1
|
||||||
|
|
||||||
|
- name: Re-health-check after rollback
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
id: healthcheck_rb
|
||||||
|
run: |
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Rollback also failed!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Post-deploy notification
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||||
|
|
||||||
|
- name: Notify failure (Telegram)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
|
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||||
|
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||||
|
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||||
|
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||||
|
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||||
|
run: |
|
||||||
|
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||||
|
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||||
|
elif [ -z "$PREV_SHA" ]; then
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||||
|
else
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||||
|
fi
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
|
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
|
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
|
||||||
# ========================================
|
# ========================================
|
||||||
deploy-pex:
|
deploy-pex:
|
||||||
name: Deploy pex.mom
|
name: Deploy pex.mom
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [bump-version]
|
needs: [telegram-gate, bump-version, build-image]
|
||||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.telegram-gate.result == 'success' &&
|
||||||
|
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
env:
|
||||||
|
DOMAIN: pex.mom
|
||||||
|
TARGET_PATH: /var/www/pex.mom
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Determine image SHA
|
||||||
uses: actions/download-artifact@v4
|
id: sha
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||||
|
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Capture currently-live SHA (for auto-rollback)
|
||||||
|
id: prev
|
||||||
|
run: |
|
||||||
|
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||||
|
echo "Previous live SHA: ${PREV:-unknown}"
|
||||||
|
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
name: web-dist
|
registry: ${{ env.REGISTRY }}
|
||||||
path: dist/
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install cosign (for verify)
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
- name: Verify image signature (cosign keyless)
|
||||||
|
env:
|
||||||
|
COSIGN_EXPERIMENTAL: '1'
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
echo "Verifying signature for $IMAGE"
|
||||||
|
cosign verify "$IMAGE" \
|
||||||
|
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||||
|
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||||
|
> /dev/null
|
||||||
|
echo "✅ Signature valid — image was built by trusted CI"
|
||||||
|
|
||||||
|
- name: Extract /dist from image
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
mkdir -p dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||||
|
|
||||||
- name: Deploy to VPS3
|
- name: Deploy to VPS3
|
||||||
uses: appleboy/scp-action@v1.0.0
|
uses: appleboy/scp-action@v1.0.0
|
||||||
@@ -172,35 +526,153 @@ jobs:
|
|||||||
target: '/var/www/pex.mom'
|
target: '/var/www/pex.mom'
|
||||||
strip_components: 1
|
strip_components: 1
|
||||||
|
|
||||||
- name: Post-deploy notification
|
- name: Health check (60s window)
|
||||||
|
id: healthcheck
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Deployed v${{ needs.bump-version.outputs.new_version }} to pex.mom"
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Auto-rollback to previous SHA
|
||||||
|
id: rollback
|
||||||
|
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||||
|
run: |
|
||||||
|
PREV="${{ steps.prev.outputs.prev }}"
|
||||||
|
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||||
|
docker pull "$IMAGE"
|
||||||
|
CID=$(docker create "$IMAGE")
|
||||||
|
rm -rf dist && mkdir dist
|
||||||
|
docker cp "$CID:/dist/." dist/
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
echo "$PREV" > dist/.deploy-sha
|
||||||
|
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: SCP rollback artifact
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
uses: appleboy/scp-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_PEX_HOST }}
|
||||||
|
username: ${{ secrets.VPS_PEX_USER }}
|
||||||
|
key: ${{ secrets.VPS_PEX_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
|
||||||
|
source: 'dist/*'
|
||||||
|
target: '/var/www/pex.mom'
|
||||||
|
strip_components: 1
|
||||||
|
|
||||||
|
- name: Re-health-check after rollback
|
||||||
|
if: steps.rollback.outcome == 'success'
|
||||||
|
id: healthcheck_rb
|
||||||
|
run: |
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
|
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "❌ Rollback also failed!"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Post-deploy notification
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||||
|
|
||||||
|
- name: Notify failure (Telegram)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
|
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||||
|
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||||
|
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||||
|
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||||
|
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||||
|
run: |
|
||||||
|
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||||
|
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||||
|
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||||
|
elif [ -z "$PREV_SHA" ]; then
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||||
|
else
|
||||||
|
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||||
|
fi
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
|
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# SECURITY CHECKS (BLOCKING)
|
# SECURITY CHECKS (BLOCKING)
|
||||||
|
# npm audit (high + critical) + TruffleHog secret scan
|
||||||
# ========================================
|
# ========================================
|
||||||
security-audit:
|
security-audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web]
|
needs: [web]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Web - npm audit (critical only)
|
- name: Web — npm audit (high + critical, production deps only)
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm audit --audit-level=critical
|
# Audit only production dependencies. Build tooling (vite, esbuild,
|
||||||
|
# vite-plugin-node-polyfills → elliptic, etc.) ships to no user, and
|
||||||
|
# advisories on those dev deps kept blocking production deploys.
|
||||||
|
npm audit --audit-level=high --omit=dev
|
||||||
|
|
||||||
- name: TruffleHog Secret Scan
|
- name: TruffleHog — PR diff (verified secrets only)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: trufflesecurity/trufflehog@main
|
||||||
|
with:
|
||||||
|
base: ${{ github.event.pull_request.base.sha }}
|
||||||
|
head: ${{ github.event.pull_request.head.sha }}
|
||||||
|
extra_args: --only-verified
|
||||||
|
|
||||||
|
- name: TruffleHog — full repo scan (verified secrets only)
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
uses: trufflesecurity/trufflehog@main
|
uses: trufflesecurity/trufflehog@main
|
||||||
with:
|
with:
|
||||||
path: ./
|
path: ./
|
||||||
extra_args: --only-verified
|
extra_args: --only-verified
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CI GATE — explicit merge-block
|
||||||
|
# All required checks must succeed (or be skipped, e.g. for rollback path).
|
||||||
|
# Branch protection on main should require this job's success.
|
||||||
|
# ========================================
|
||||||
|
ci-gate:
|
||||||
|
name: CI Gate ✅
|
||||||
|
runs-on: pwap-runner
|
||||||
|
needs: [web, security-audit]
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify all required jobs succeeded or were intentionally skipped
|
||||||
|
run: |
|
||||||
|
results='${{ toJSON(needs) }}'
|
||||||
|
echo "$results" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
needs = json.load(sys.stdin)
|
||||||
|
failed = [name for name, job in needs.items() if job['result'] not in ('success', 'skipped')]
|
||||||
|
if failed:
|
||||||
|
print('❌ Required jobs failed: ' + ', '.join(failed))
|
||||||
|
sys.exit(1)
|
||||||
|
print('✅ All required CI jobs passed or skipped')
|
||||||
|
"
|
||||||
|
|||||||
-1
Submodule exchange deleted from bb3bc812ed
@@ -0,0 +1,53 @@
|
|||||||
|
# pwap/web — Static SPA build for distribution.
|
||||||
|
# Build context is the pwap repo ROOT (not web/) because vite aliases like
|
||||||
|
# @pezkuwi/utils, @shared/* resolve to ../shared/* — both web/ and shared/
|
||||||
|
# must be in the build context.
|
||||||
|
# Stage 1: build with Node. Stage 2: pure dist/ in busybox (smallest possible
|
||||||
|
# attacker surface — no shell, no package manager, no node runtime).
|
||||||
|
# Tag the resulting image with the git SHA in CI so rollback is just
|
||||||
|
# "pull pwap-web:<old-sha>".
|
||||||
|
|
||||||
|
# ─── Stage 1: Build ────────────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /build/web
|
||||||
|
|
||||||
|
# Copy package files first to leverage Docker layer cache when only src changes
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy shared/ first (less frequently changed), then web/ source
|
||||||
|
COPY shared/ /build/shared/
|
||||||
|
COPY web/ /build/web/
|
||||||
|
|
||||||
|
# Build args for environment-specific values (passed from CI)
|
||||||
|
ARG VITE_NETWORK=MAINNET
|
||||||
|
ARG VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||||
|
ARG VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||||
|
ARG VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||||
|
ARG VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||||
|
ARG VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||||
|
ARG VITE_SUPABASE_URL
|
||||||
|
ARG VITE_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
ENV VITE_NETWORK=$VITE_NETWORK
|
||||||
|
ENV VITE_WS_ENDPOINT=$VITE_WS_ENDPOINT
|
||||||
|
ENV VITE_WS_ENDPOINT_FALLBACK_1=$VITE_WS_ENDPOINT_FALLBACK_1
|
||||||
|
ENV VITE_ASSET_HUB_ENDPOINT=$VITE_ASSET_HUB_ENDPOINT
|
||||||
|
ENV VITE_PEOPLE_CHAIN_ENDPOINT=$VITE_PEOPLE_CHAIN_ENDPOINT
|
||||||
|
ENV VITE_WALLETCONNECT_PROJECT_ID=$VITE_WALLETCONNECT_PROJECT_ID
|
||||||
|
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
|
||||||
|
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ─── Stage 2: Distribution image ───────────────────────────────
|
||||||
|
# busybox:musl gives us a tiny base (~1.5MB) with a shell for `cp` operations
|
||||||
|
# during deploy extraction, but no npm/curl/wget/ssh — minimal attack surface
|
||||||
|
# if the image were ever exposed.
|
||||||
|
FROM busybox:musl
|
||||||
|
WORKDIR /dist
|
||||||
|
COPY --from=builder /build/web/dist /dist
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/pezkuwichain/pwap"
|
||||||
|
LABEL org.opencontainers.image.description="pwap/web static SPA — Pezkuwi wallet/exchange frontend"
|
||||||
|
LABEL org.opencontainers.image.licenses="proprietary"
|
||||||
|
CMD ["sh", "-c", "echo 'pwap-web image — extract /dist via: docker create + docker cp'; sleep infinity"]
|
||||||
Generated
+113
-84
@@ -109,6 +109,7 @@
|
|||||||
"typescript-eslint": "^8.0.1",
|
"typescript-eslint": "^8.0.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
"vite-plugin-node-polyfills": "^0.25.0",
|
||||||
|
"vite-plugin-subresource-integrity": "^0.0.12",
|
||||||
"vitest": "^4.0.10"
|
"vitest": "^4.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3507,9 +3508,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.3",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
||||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
"integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -5070,31 +5071,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
|
||||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.1.0",
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.1.8",
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.1.8",
|
||||||
"chai": "^6.2.1",
|
"chai": "^6.2.2",
|
||||||
"tinyrainbow": "^3.0.3"
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
|
||||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.1.8",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.21"
|
"magic-string": "^0.30.21"
|
||||||
},
|
},
|
||||||
@@ -5103,7 +5104,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"msw": "^2.4.9",
|
"msw": "^2.4.9",
|
||||||
"vite": "^6.0.0 || ^7.0.0-0"
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"msw": {
|
"msw": {
|
||||||
@@ -5125,26 +5126,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
|
||||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyrainbow": "^3.0.3"
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
|
||||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.1.8",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -5152,13 +5153,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
|
||||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.1.8",
|
||||||
|
"@vitest/utils": "4.1.8",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -5167,9 +5169,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
|
||||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -5177,14 +5179,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
|
||||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.1.8",
|
||||||
"tinyrainbow": "^3.0.3"
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
@@ -6567,13 +6570,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserify-sign": {
|
"node_modules/browserify-sign": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz",
|
||||||
"integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==",
|
"integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bn.js": "^5.2.2",
|
"bn.js": "^5.2.3",
|
||||||
"browserify-rsa": "^4.1.1",
|
"browserify-rsa": "^4.1.1",
|
||||||
"create-hash": "^1.2.0",
|
"create-hash": "^1.2.0",
|
||||||
"create-hmac": "^1.1.7",
|
"create-hmac": "^1.1.7",
|
||||||
@@ -7028,6 +7031,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cookie-es": {
|
"node_modules/cookie-es": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
||||||
@@ -7859,9 +7869,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -11147,9 +11157,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11367,12 +11377,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.3",
|
"version": "6.30.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
|
||||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
"integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2"
|
"@remix-run/router": "1.23.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -11382,13 +11392,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.30.3",
|
"version": "6.30.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
|
||||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
"integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2",
|
"@remix-run/router": "1.23.3",
|
||||||
"react-router": "6.30.3"
|
"react-router": "6.30.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -12237,9 +12247,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/std-env": {
|
"node_modules/std-env": {
|
||||||
"version": "3.10.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
||||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -12696,9 +12706,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyrainbow": {
|
"node_modules/tinyrainbow": {
|
||||||
"version": "3.0.3",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -13259,6 +13269,13 @@
|
|||||||
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-subresource-integrity": {
|
||||||
|
"version": "0.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-subresource-integrity/-/vite-plugin-subresource-integrity-0.0.12.tgz",
|
||||||
|
"integrity": "sha512-geKEo1KgGA56G8CciaoKA3Yf7ckpR23zSuSW802xrisW6vnH+dAYjKXZygEcmFKfsOe0+r3uG7Oz9eFEwhdjxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -13291,31 +13308,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "4.0.18",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
|
||||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.18",
|
"@vitest/expect": "4.1.8",
|
||||||
"@vitest/mocker": "4.0.18",
|
"@vitest/mocker": "4.1.8",
|
||||||
"@vitest/pretty-format": "4.0.18",
|
"@vitest/pretty-format": "4.1.8",
|
||||||
"@vitest/runner": "4.0.18",
|
"@vitest/runner": "4.1.8",
|
||||||
"@vitest/snapshot": "4.0.18",
|
"@vitest/snapshot": "4.1.8",
|
||||||
"@vitest/spy": "4.0.18",
|
"@vitest/spy": "4.1.8",
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.1.8",
|
||||||
"es-module-lexer": "^1.7.0",
|
"es-module-lexer": "^2.0.0",
|
||||||
"expect-type": "^1.2.2",
|
"expect-type": "^1.3.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"obug": "^2.1.1",
|
"obug": "^2.1.1",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"std-env": "^3.10.0",
|
"std-env": "^4.0.0-rc.1",
|
||||||
"tinybench": "^2.9.0",
|
"tinybench": "^2.9.0",
|
||||||
"tinyexec": "^1.0.2",
|
"tinyexec": "^1.0.2",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"tinyrainbow": "^3.0.3",
|
"tinyrainbow": "^3.1.0",
|
||||||
"vite": "^6.0.0 || ^7.0.0",
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -13331,12 +13348,15 @@
|
|||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
"@vitest/browser-playwright": "4.0.18",
|
"@vitest/browser-playwright": "4.1.8",
|
||||||
"@vitest/browser-preview": "4.0.18",
|
"@vitest/browser-preview": "4.1.8",
|
||||||
"@vitest/browser-webdriverio": "4.0.18",
|
"@vitest/browser-webdriverio": "4.1.8",
|
||||||
"@vitest/ui": "4.0.18",
|
"@vitest/coverage-istanbul": "4.1.8",
|
||||||
|
"@vitest/coverage-v8": "4.1.8",
|
||||||
|
"@vitest/ui": "4.1.8",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@edge-runtime/vm": {
|
"@edge-runtime/vm": {
|
||||||
@@ -13357,6 +13377,12 @@
|
|||||||
"@vitest/browser-webdriverio": {
|
"@vitest/browser-webdriverio": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"@vitest/ui": {
|
"@vitest/ui": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
@@ -13365,6 +13391,9 @@
|
|||||||
},
|
},
|
||||||
"jsdom": {
|
"jsdom": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -13606,9 +13635,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.19.0",
|
"version": "8.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
+3
-1
@@ -120,7 +120,8 @@
|
|||||||
"@pezkuwi/x-textdecoder": "^14.0.25",
|
"@pezkuwi/x-textdecoder": "^14.0.25",
|
||||||
"@pezkuwi/x-textencoder": "^14.0.25",
|
"@pezkuwi/x-textencoder": "^14.0.25",
|
||||||
"@pezkuwi/x-ws": "^14.0.25",
|
"@pezkuwi/x-ws": "^14.0.25",
|
||||||
"@pezkuwi/networks": "^14.0.25"
|
"@pezkuwi/networks": "^14.0.25",
|
||||||
|
"elliptic": "^6.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
@@ -147,6 +148,7 @@
|
|||||||
"typescript-eslint": "^8.0.1",
|
"typescript-eslint": "^8.0.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
"vite-plugin-node-polyfills": "^0.25.0",
|
||||||
|
"vite-plugin-subresource-integrity": "^0.0.12",
|
||||||
"vitest": "^4.0.10"
|
"vitest": "^4.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||||
|
import { Pez20Badge } from './Pez20Badge';
|
||||||
import { AddTokenModal } from './AddTokenModal';
|
import { AddTokenModal } from './AddTokenModal';
|
||||||
import { TransferModal } from './TransferModal';
|
import { TransferModal } from './TransferModal';
|
||||||
import { XCMTeleportModal } from './XCMTeleportModal';
|
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||||
@@ -811,6 +812,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||||
{t('balance.pezBalance')}
|
{t('balance.pezBalance')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Pez20Badge className="flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -853,6 +855,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
<CardTitle className="text-lg font-medium text-gray-300">
|
<CardTitle className="text-lg font-medium text-gray-300">
|
||||||
{t('balance.usdtBalance')}
|
{t('balance.usdtBalance')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Pez20Badge className="flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small pill marking a token as a Pezkuwi token-standard asset.
|
||||||
|
* PEZ-20 = fungible standard (pallet-assets on Asset Hub), PEZ-721 = NFT standard.
|
||||||
|
* See docs.pezkuwichain.io → Token Standards.
|
||||||
|
*/
|
||||||
|
export const Pez20Badge: React.FC<{ standard?: 'PEZ-20' | 'PEZ-721'; className?: string }> = ({
|
||||||
|
standard = 'PEZ-20',
|
||||||
|
className = '',
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
href="https://docs.pezkuwichain.io/token-standards"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={`${standard} token standard on Pezkuwi Asset Hub`}
|
||||||
|
className={
|
||||||
|
'inline-flex items-center rounded-full border border-blue-500/40 bg-blue-500/10 ' +
|
||||||
|
'px-2 py-0.5 text-[10px] font-semibold tracking-wide text-blue-300 ' +
|
||||||
|
'hover:bg-blue-500/20 transition-colors no-underline ' +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{standard}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Pez20Badge;
|
||||||
@@ -15,6 +15,8 @@ interface ChainStats {
|
|||||||
validators: number;
|
validators: number;
|
||||||
nominators: number;
|
nominators: number;
|
||||||
collators: number;
|
collators: number;
|
||||||
|
collatorsAH: number;
|
||||||
|
collatorsPeople: number;
|
||||||
activeProposals: number;
|
activeProposals: number;
|
||||||
totalVoters: number;
|
totalVoters: number;
|
||||||
citizenCount: number;
|
citizenCount: number;
|
||||||
@@ -325,6 +327,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
const [stats, setStats] = useState<ChainStats>({
|
const [stats, setStats] = useState<ChainStats>({
|
||||||
latestBlock: 0, finalizedBlock: 0, blockHash: '',
|
latestBlock: 0, finalizedBlock: 0, blockHash: '',
|
||||||
peers: 0, validators: 0, nominators: 0, collators: 0,
|
peers: 0, validators: 0, nominators: 0, collators: 0,
|
||||||
|
collatorsAH: 0, collatorsPeople: 0,
|
||||||
activeProposals: 0, totalVoters: 0, citizenCount: 0,
|
activeProposals: 0, totalVoters: 0, citizenCount: 0,
|
||||||
tokensStakedPct: '—',
|
tokensStakedPct: '—',
|
||||||
});
|
});
|
||||||
@@ -417,12 +420,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
const validators = sessionVals.length;
|
const validators = sessionVals.length;
|
||||||
setStats(prev => ({ ...prev, activeProposals, totalVoters, validators }));
|
setStats(prev => ({ ...prev, activeProposals, totalVoters, validators }));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
// Nominators/staking migrated to Asset Hub — counted in the Asset Hub effect below.
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const nomCount = await (api.query.staking as any).counterForNominators?.();
|
|
||||||
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
|
||||||
} catch {}
|
|
||||||
})();
|
})();
|
||||||
}, [api, isApiReady]);
|
}, [api, isApiReady]);
|
||||||
|
|
||||||
@@ -448,10 +446,18 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// Nominators live on Asset Hub after the staking migration (AHM).
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const collCount = await (assetHubApi.query.collatorSelection as any)?.candidates?.();
|
const nomCount = await (assetHubApi.query.staking as any)?.counterForNominators?.();
|
||||||
if (collCount != null) setStats(prev => ({ ...prev, collators: collCount.length }));
|
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Collators are the invulnerable set (not staking candidates, which are empty).
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const inv = await (assetHubApi.query.collatorSelection as any)?.invulnerables?.();
|
||||||
|
if (inv != null) setStats(prev => ({ ...prev, collatorsAH: inv.length, collators: inv.length + prev.collatorsPeople }));
|
||||||
} catch {}
|
} catch {}
|
||||||
})();
|
})();
|
||||||
}, [assetHubApi, isAssetHubReady]);
|
}, [assetHubApi, isAssetHubReady]);
|
||||||
@@ -465,6 +471,13 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.();
|
const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.();
|
||||||
if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length }));
|
if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length }));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// People Chain also runs invulnerable collators — add them to the total.
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const inv = await (peopleApi.query.collatorSelection as any)?.invulnerables?.();
|
||||||
|
if (inv != null) setStats(prev => ({ ...prev, collatorsPeople: inv.length, collators: prev.collatorsAH + inv.length }));
|
||||||
|
} catch {}
|
||||||
})();
|
})();
|
||||||
}, [peopleApi, isPeopleReady]);
|
}, [peopleApi, isPeopleReady]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
@@ -12,7 +12,6 @@ const BEREKETLI_API = `${BEREKETLI_URL}/v1`;
|
|||||||
*/
|
*/
|
||||||
export default function Bereketli() {
|
export default function Bereketli() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -20,8 +19,10 @@ export default function Bereketli() {
|
|||||||
const {
|
const {
|
||||||
data: { session },
|
data: { session },
|
||||||
} = await supabase.auth.getSession();
|
} = await supabase.auth.getSession();
|
||||||
|
// Not signed in: skip SSO and send the user to the Bereketli site,
|
||||||
|
// which handles its own login. Never dead-end on this interstitial.
|
||||||
if (!session?.access_token) {
|
if (!session?.access_token) {
|
||||||
setError(t('bereketli.noSession', 'Lütfen önce giriş yapın'));
|
window.location.href = BEREKETLI_URL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,27 +47,14 @@ export default function Bereketli() {
|
|||||||
});
|
});
|
||||||
window.location.href = `${BEREKETLI_URL}/app?auth=${btoa(params.toString())}`;
|
window.location.href = `${BEREKETLI_URL}/app?auth=${btoa(params.toString())}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Bağlantı hatası');
|
// SSO failed (expired token, network, etc.) — fall back to the public
|
||||||
|
// Bereketli site instead of stranding the user on app.pezkuwichain.io.
|
||||||
|
if (import.meta.env.DEV) console.warn('Bereketli SSO failed, falling back:', err);
|
||||||
|
window.location.href = BEREKETLI_URL;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-6">
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<p className="text-red-400 text-sm">{error}</p>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
className="inline-block px-4 py-2 bg-green-600 text-white rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
{t('common.backToHome', 'Ana Sayfaya Dön')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||||
<div className="text-center space-y-3">
|
<div className="text-center space-y-3">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
|
import subresourceIntegrity from 'vite-plugin-subresource-integrity';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ command }) => ({
|
export default defineConfig(({ command }) => ({
|
||||||
@@ -38,6 +39,10 @@ export default defineConfig(({ command }) => ({
|
|||||||
},
|
},
|
||||||
protocolImports: true,
|
protocolImports: true,
|
||||||
}),
|
}),
|
||||||
|
// SRI: production build sırasında <script>/<link> tag'lerine
|
||||||
|
// sha384 integrity hash ekle. CDN/proxy compromise olsa bile
|
||||||
|
// tampered asset browser tarafından load edilmez.
|
||||||
|
command === 'build' ? subresourceIntegrity({ algorithm: 'sha384' }) : null,
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
mainFields: ['browser', 'module', 'main', 'exports'],
|
mainFields: ['browser', 'module', 'main', 'exports'],
|
||||||
|
|||||||
Reference in New Issue
Block a user