mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-20 03:51:00 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51eecf9e08 | |||
| 78e93e9766 | |||
| 83d66feacc | |||
| d6ace14e70 | |||
| 2cbfd21539 | |||
| f7c070e45b | |||
| 06ed9734c6 | |||
| d93d4c6cd0 |
@@ -21,7 +21,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze ${{ matrix.language }}
|
name: Analyze ${{ matrix.language }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -32,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
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
build-image:
|
build-image:
|
||||||
name: Build & Push Image
|
name: Build & Push Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web, telegram-gate]
|
needs: [web, telegram-gate]
|
||||||
if: |
|
if: |
|
||||||
github.ref == 'refs/heads/main' &&
|
github.ref == 'refs/heads/main' &&
|
||||||
@@ -101,14 +101,21 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
id-token: write # cosign keyless signing via Sigstore OIDC
|
||||||
outputs:
|
outputs:
|
||||||
image_sha: ${{ steps.meta.outputs.image_sha }}
|
image_sha: ${{ steps.meta.outputs.image_sha }}
|
||||||
|
image_digest: ${{ steps.build.outputs.digest }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
- 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
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -125,6 +132,7 @@ jobs:
|
|||||||
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
|
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
@@ -146,6 +154,18 @@ jobs:
|
|||||||
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
|
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
|
||||||
provenance: false
|
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
|
# TELEGRAM CEO APPROVAL GATE
|
||||||
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
|
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
|
||||||
@@ -215,7 +235,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
bump-version:
|
bump-version:
|
||||||
name: Bump Version
|
name: Bump Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web, security-audit, telegram-gate, build-image]
|
needs: [web, security-audit, telegram-gate, build-image]
|
||||||
# Skip on rollback (workflow_dispatch with rollback_to set)
|
# Skip on rollback (workflow_dispatch with rollback_to set)
|
||||||
if: |
|
if: |
|
||||||
@@ -261,7 +281,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
deploy-app:
|
deploy-app:
|
||||||
name: Deploy app.pezkuwichain.io
|
name: Deploy app.pezkuwichain.io
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [telegram-gate, bump-version, build-image]
|
needs: [telegram-gate, bump-version, build-image]
|
||||||
if: |
|
if: |
|
||||||
always() &&
|
always() &&
|
||||||
@@ -286,6 +306,14 @@ jobs:
|
|||||||
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
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
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -293,6 +321,24 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
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
|
- name: Extract /dist from image
|
||||||
run: |
|
run: |
|
||||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
@@ -301,7 +347,8 @@ jobs:
|
|||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
docker cp "$CID:/dist/." dist/
|
docker cp "$CID:/dist/." dist/
|
||||||
docker rm "$CID" >/dev/null
|
docker rm "$CID" >/dev/null
|
||||||
echo "Extracted dist/ contents:"
|
# 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
|
ls -la dist/ | head -10
|
||||||
|
|
||||||
- name: Deploy to DEV VPS
|
- name: Deploy to DEV VPS
|
||||||
@@ -329,6 +376,48 @@ jobs:
|
|||||||
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||||
exit 1
|
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
|
- name: Post-deploy notification
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
@@ -339,17 +428,29 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
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: |
|
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" \
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
-d "chat_id=${CEO_CHAT_ID}" \
|
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||||
-d "text=❌ pwap/web deploy FAILED: ${{ env.DOMAIN }} (sha ${{ steps.sha.outputs.sha }}). Health check did not pass after deploy. Manual rollback needed: gh workflow run quality-gate.yml -f rollback_to=<previous-sha>"
|
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# 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: [telegram-gate, bump-version, build-image]
|
needs: [telegram-gate, bump-version, build-image]
|
||||||
if: |
|
if: |
|
||||||
always() &&
|
always() &&
|
||||||
@@ -373,6 +474,13 @@ jobs:
|
|||||||
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
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
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -380,6 +488,23 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
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
|
- name: Extract /dist from image
|
||||||
run: |
|
run: |
|
||||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||||
@@ -388,6 +513,7 @@ jobs:
|
|||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
docker cp "$CID:/dist/." dist/
|
docker cp "$CID:/dist/." dist/
|
||||||
docker rm "$CID" >/dev/null
|
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
|
||||||
@@ -401,6 +527,7 @@ jobs:
|
|||||||
strip_components: 1
|
strip_components: 1
|
||||||
|
|
||||||
- name: Health check (60s window)
|
- name: Health check (60s window)
|
||||||
|
id: healthcheck
|
||||||
run: |
|
run: |
|
||||||
for i in 1 2 3 4 5 6; do
|
for i in 1 2 3 4 5 6; do
|
||||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||||
@@ -413,6 +540,47 @@ jobs:
|
|||||||
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||||
exit 1
|
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
|
- name: Post-deploy notification
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
@@ -423,10 +591,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
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: |
|
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" \
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
-d "chat_id=${CEO_CHAT_ID}" \
|
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||||
-d "text=❌ pwap/web deploy FAILED: ${{ env.DOMAIN }} (sha ${{ steps.sha.outputs.sha }}). Health check did not pass. Rollback: gh workflow run quality-gate.yml -f rollback_to=<previous-sha>"
|
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# SECURITY CHECKS (BLOCKING)
|
# SECURITY CHECKS (BLOCKING)
|
||||||
@@ -434,7 +614,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
security-audit:
|
security-audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web]
|
needs: [web]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -448,11 +628,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Web — npm audit (high + critical)
|
- 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=high
|
# 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 — PR diff (verified secrets only)
|
- name: TruffleHog — PR diff (verified secrets only)
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
@@ -476,7 +659,7 @@ jobs:
|
|||||||
# ========================================
|
# ========================================
|
||||||
ci-gate:
|
ci-gate:
|
||||||
name: CI Gate ✅
|
name: CI Gate ✅
|
||||||
runs-on: ubuntu-latest
|
runs-on: pwap-runner
|
||||||
needs: [web, security-audit]
|
needs: [web, security-audit]
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
|
|||||||
-1
Submodule exchange deleted from bb3bc812ed
+1
-1
@@ -46,7 +46,7 @@ RUN npm run build
|
|||||||
# if the image were ever exposed.
|
# if the image were ever exposed.
|
||||||
FROM busybox:musl
|
FROM busybox:musl
|
||||||
WORKDIR /dist
|
WORKDIR /dist
|
||||||
COPY --from=builder /build/dist /dist
|
COPY --from=builder /build/web/dist /dist
|
||||||
LABEL org.opencontainers.image.source="https://github.com/pezkuwichain/pwap"
|
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.description="pwap/web static SPA — Pezkuwi wallet/exchange frontend"
|
||||||
LABEL org.opencontainers.image.licenses="proprietary"
|
LABEL org.opencontainers.image.licenses="proprietary"
|
||||||
|
|||||||
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