diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml
index 7016bbca..8ff3dbbe 100644
--- a/.github/workflows/quality-gate.yml
+++ b/.github/workflows/quality-gate.yml
@@ -58,12 +58,20 @@ jobs:
- name: Install dependencies
working-directory: ./web
- run: npm install
+ run: npm ci
- name: Run Linter
working-directory: ./web
run: npm run lint
+ # Typecheck gate. Currently non-blocking (continue-on-error) because the
+ # existing codebase still has pre-existing type errors. Once the count
+ # reaches zero, remove `continue-on-error` to make this a hard merge block.
+ - name: Typecheck
+ working-directory: ./web
+ continue-on-error: true
+ run: npm run typecheck
+
- name: Run Tests
working-directory: ./web
run: npm run test
@@ -85,6 +93,48 @@ jobs:
name: web-dist
path: web/dist/
+ # ========================================
+ # BACKEND - INDEXER SERVICE (lint/smoke/audit)
+ # No build step exists (plain ESM node service); we lockfile-enforce install,
+ # syntax-check the entry point, and audit. The audit is currently non-blocking
+ # because the express@5 + sqlite3(node-gyp) trees carry transitive advisories
+ # that need a broader dependency upgrade; flip continue-on-error off once the
+ # tree is clean.
+ # ========================================
+ backend:
+ name: Backend Indexer
+ runs-on: pwap-runner
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Cache npm dependencies
+ uses: actions/cache@v4
+ with:
+ path: backend/node_modules
+ key: ${{ runner.os }}-backend-${{ hashFiles('backend/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-backend-
+
+ - name: Install dependencies
+ working-directory: ./backend
+ run: npm ci
+
+ - name: Syntax check (entry point)
+ working-directory: ./backend
+ run: node --check src/index.js
+
+ - name: npm audit (high + critical, production deps)
+ working-directory: ./backend
+ continue-on-error: true
+ run: npm audit --audit-level=high --omit=dev
+
# ========================================
# BUILD & PUSH DOCKER IMAGE TO GHCR
# Immutable artifact for audit + rollback (vs ephemeral GHA artifact).
@@ -631,7 +681,7 @@ jobs:
- name: Web — npm audit (high + critical, production deps only)
working-directory: ./web
run: |
- npm install
+ npm ci
# 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.
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 7131404a..d8722758 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -2994,9 +2994,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.7",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
- "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -3215,9 +3215,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.19.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
- "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/backend/package.json b/backend/package.json
index c7a7048b..c6ed1b6c 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -9,7 +9,8 @@
"dev": "node --watch src/index.js"
},
"overrides": {
- "tar": "^7.4.3",
+ "tar": "^7.5.22",
+ "ws": "^8.21.1",
"@pezkuwi/api": "^16.5.23",
"@pezkuwi/api-augment": "^16.5.23",
"@pezkuwi/api-base": "^16.5.23",
diff --git a/package.json b/package.json
index f4f61775..d5dbff90 100644
--- a/package.json
+++ b/package.json
@@ -6,16 +6,16 @@
"scripts": {
"build": "npm run build:web && npm run build:sdk-ui && npm run build:mobile",
"build:web": "cd web && npm run build",
- "build:sdk-ui": "cd /home/mamostehp/pezkuwi-sdk-ui && yarn build",
+ "build:sdk-ui": "cd \"${SDK_UI_DIR:-../pezkuwi-sdk-ui}\" 2>/dev/null && yarn build || echo 'sdk-ui: skipped (set SDK_UI_DIR to the pezkuwi-sdk-ui checkout to enable)'",
"build:mobile": "cd mobile && npx expo export --platform web",
"build:parallel": "concurrently \"npm run build:web\" \"npm run build:sdk-ui\"",
"dev": "concurrently \"npm run dev:web\" \"npm run dev:mobile\"",
"dev:web": "cd web && npm run dev",
- "dev:sdk-ui": "cd /home/mamostehp/pezkuwi-sdk-ui && yarn start",
+ "dev:sdk-ui": "cd \"${SDK_UI_DIR:-../pezkuwi-sdk-ui}\" && yarn start",
"dev:mobile": "cd mobile && npm run dev",
"install:all": "npm run install:web && npm run install:sdk-ui && npm run install:mobile && npm run install:backend",
"install:web": "cd web && npm install",
- "install:sdk-ui": "cd /home/mamostehp/pezkuwi-sdk-ui && yarn install",
+ "install:sdk-ui": "cd \"${SDK_UI_DIR:-../pezkuwi-sdk-ui}\" 2>/dev/null && yarn install || echo 'sdk-ui: skipped (set SDK_UI_DIR to the pezkuwi-sdk-ui checkout to enable)'",
"install:mobile": "cd mobile && npm install",
"install:backend": "cd backend && npm install",
"lint": "npm run lint:web && npm run lint:mobile",
diff --git a/web/Dockerfile b/web/Dockerfile
index 6b5b51b9..facd1c91 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -49,5 +49,5 @@ 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"
+LABEL org.opencontainers.image.licenses="MIT"
CMD ["sh", "-c", "echo 'pwap-web image — extract /dist via: docker create + docker cp'; sleep infinity"]
diff --git a/web/generate-docs-structure.cjs b/web/generate-docs-structure.cjs
index b74fa275..40e29bb6 100644
--- a/web/generate-docs-structure.cjs
+++ b/web/generate-docs-structure.cjs
@@ -17,8 +17,23 @@ const rustdocDestPath = path.join(publicPath, 'sdk_docs'); // Destination for BU
const structureOutputPath = path.join(publicPath, 'docs-structure.json');
const rustdocBuildOutputPath = path.join(pezkuwiSdkRoot, 'target', 'doc'); // Output of cargo doc
-// Absolute path to rustup (used to build rustdoc)
-const rustupPath = '/home/mamostehp/.cargo/bin/rustup';
+// Path to rustup (used to build rustdoc). Resolve dynamically instead of
+// hardcoding a per-machine absolute path: prefer $RUSTUP_PATH, then whatever
+// is on PATH, then the conventional ~/.cargo/bin/rustup, then bare 'rustup'.
+function resolveRustupPath() {
+ if (process.env.RUSTUP_PATH) return process.env.RUSTUP_PATH;
+ try {
+ const which = spawnSync('bash', ['-lc', 'command -v rustup'], { encoding: 'utf8' });
+ if (which.status === 0 && which.stdout && which.stdout.trim()) {
+ return which.stdout.trim();
+ }
+ } catch { /* fall through */ }
+ const home = process.env.HOME || require('os').homedir();
+ const cargoRustup = path.join(home, '.cargo', 'bin', 'rustup');
+ if (fs.existsSync(cargoRustup)) return cargoRustup;
+ return 'rustup';
+}
+const rustupPath = resolveRustupPath();
// Path to the rebranding script (now .cjs)
const rebrandScriptPath = path.join(__dirname, 'rebrand-rustdoc.cjs');
diff --git a/web/package-lock.json b/web/package-lock.json
index 72ab23f1..7073b55b 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -58,7 +58,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
- "dompurify": "^3.3.1",
+ "dompurify": "^3.4.12",
"embla-carousel-react": "^8.3.0",
"highlight.js": "^11.9.0",
"i18next": "^23.7.6",
@@ -103,7 +103,7 @@
"globals": "^15.15.0",
"husky": "^9.1.7",
"jsdom": "^27.2.0",
- "postcss": "^8.4.47",
+ "postcss": "^8.5.23",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
@@ -7651,9 +7651,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.4.10",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz",
- "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
+ "version": "3.4.12",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
+ "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -10054,9 +10054,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"funding": [
{
"type": "github",
@@ -10800,9 +10800,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.13",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
- "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"funding": [
{
"type": "opencollective",
@@ -10819,7 +10819,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
diff --git a/web/package.json b/web/package.json
index 17307b1c..b247cadd 100644
--- a/web/package.json
+++ b/web/package.json
@@ -8,6 +8,7 @@
"dev": "cp .env.development .env && vite",
"prebuild": "node generate-docs-structure.cjs",
"build": "vite build",
+ "typecheck": "tsc --noEmit -p tsconfig.app.json",
"build:telegram": "vite build --config vite.config.telegram.ts",
"build:dev": "cp .env.development .env && vite build",
"build:alfa": "cp .env.alfa .env && vite build",
@@ -70,7 +71,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
- "dompurify": "^3.3.1",
+ "dompurify": "^3.4.12",
"embla-carousel-react": "^8.3.0",
"highlight.js": "^11.9.0",
"i18next": "^23.7.6",
@@ -142,7 +143,7 @@
"globals": "^15.15.0",
"husky": "^9.1.7",
"jsdom": "^27.2.0",
- "postcss": "^8.4.47",
+ "postcss": "^8.5.23",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
diff --git a/web/src/components/notifications/NotificationBell.tsx b/web/src/components/notifications/NotificationBell.tsx
index 1dae87de..daed1af2 100644
--- a/web/src/components/notifications/NotificationBell.tsx
+++ b/web/src/components/notifications/NotificationBell.tsx
@@ -29,11 +29,14 @@ export default function NotificationBell() {
const [open, setOpen] = useState(false);
useEffect(() => {
- if (user) {
- loadNotifications();
+ if (!user) return;
- subscribeToNotifications();
- }
+ loadNotifications();
+ // Return the unsubscribe callback so the realtime channel is torn down
+ // on unmount / when the user changes — otherwise channels leak and
+ // setState fires after unmount.
+ const unsubscribe = subscribeToNotifications();
+ return unsubscribe;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
@@ -55,7 +58,7 @@ export default function NotificationBell() {
const subscribeToNotifications = () => {
const channel = supabase
- .channel('notifications')
+ .channel(`notifications-${user?.id}`)
.on(
'postgres_changes',
{
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 9825ce79..6899153e 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -26,6 +26,10 @@ if (typeof window !== 'undefined' && import.meta.env.PROD) {
};
}
+// Augment @pezkuwi/api types so api.query/tx/rpc return typed values
+// instead of the generic `Codec`. Must be imported before any api usage.
+import '@pezkuwi/api-augment';
+
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
diff --git a/web/src/pages/ProfileSettings.tsx b/web/src/pages/ProfileSettings.tsx
index 09a4be93..9faa07fe 100644
--- a/web/src/pages/ProfileSettings.tsx
+++ b/web/src/pages/ProfileSettings.tsx
@@ -398,8 +398,8 @@ export default function ProfileSettings() {
English
Türkçe
العربية
- Kurdî
- کوردی
+ Kurdî (Kurmancî)
+ کوردی (سۆرانی)
فارسی
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 6d8b7e36..61eae8f3 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -66,6 +66,9 @@ export default defineConfig(({ command }) => ({
optimizeDeps: {
include: ['@pezkuwi/util-crypto', '@pezkuwi/util', '@pezkuwi/api', '@pezkuwi/extension-dapp', '@pezkuwi/keyring', 'buffer'],
},
+ // Strip all console.* and debugger statements from production bundles so the
+ // 600+ dev-time console calls never ship. Dev keeps full logging.
+ esbuild: command === 'build' ? { drop: ['console', 'debugger'] } : {},
build: {
rollupOptions: {
external: [],