* Send the Rewshenbir button to rewshenbir.pezkiwi.app
The app grid, the mobile home and the landing page's pallet list all linked
https://rewshenbir.pezkuwi.app -- pezkuwi, not pezkiwi. That name does not
resolve, so every reader who tapped Rewshenbir reached nothing. The sibling
links (bereketli, buy-sell, kurdishtts) already use pezkiwi.app.
* Keep the source of the interim Rewshenbir landing page in the repository
rewshenbir.pezkiwi.app now answers with a page built from the design canvas
(tokens, thesis, the RewshenbirMark logo) instead of nothing. It lives in the
repository so it is not a file that exists only on the host.
Three changes, all pointing the same way: one published document, fed from one source.
`web/public/Whitepaper.pdf` was a second copy of a document that also lives on
pezkuwichain.io, in the landing tree, on three servers and in the repository the
document describes. The copies had diverged; the one bundled here was built in June and
still named a licence the project does not use and a pallet the tree does not contain.
A copy in a bundle is the hardest kind to keep current, because nothing fails when it
goes stale. It is removed, and the documentation card now links to the canonical URL, so
this application shows whatever is published rather than whatever was bundled. The
deployed hosts redirect `/Whitepaper.pdf` to the same place, so existing links keep
working.
The web build also began by cloning `pezkuwi-sdk`, first from the self-hosted GitLab and
then from GitHub. Neither is reachable: the GitLab path answers 500 to both the browser
and `git ls-remote`, and the GitHub repository is disabled. Nothing here changed -- the
workflow had not run since the second of August and the sources rotted underneath it.
The clone fed `generate-docs-structure.cjs`, which already treats the SDK as optional
and completes without it, and whose output is not served: the in-app documentation route
redirects to docs.pezkuwichain.io, which is why `.gitignore` leaves those artifacts
untracked. Swapping in another URL would not have helped either, since the run is
unauthenticated and the surviving repository is private. The step is removed.
Finally, `npm audit` reported five advisories against production dependencies, one high.
None arrived with this branch; a month of them surfaced at once on the first run.
`npm audit fix` without `--force` clears the high one and leaves two moderate, below what
the gate measures, and moves only `package-lock.json` -- no declared range changes.
`--force` was not used: it has broken this workspace's Solana dependencies before.
Verified: full pipeline green -- Web App, Backend Indexer, Migration test, Security
Audit, CodeQL and the deploy-gate wiring check.
Driving the real login page in a browser showed what shipping this actually
looks like when Cloudflare decides a visitor must click: the submit button sits
on 'Signing in...' while an unnoticed 'Verify you are human' checkbox waits in
the bottom-right corner. Nothing tells the user it is there. They conclude login
is broken, and they are not wrong.
Two changes:
- The challenge is centred over a dimmed backdrop while it is visible, and
returns to the corner once answered. Detected by measuring the container
rather than by Turnstile's interactive callbacks: those callbacks are not in
the documentation this was written against, and a callback name that turns out
to be wrong fails silently — leaving the challenge in the corner, which is the
bug being fixed.
- A 90 second cap on waiting for a token. Without it, a challenge nobody answers
leaves the button spinning forever with no error and no way back. Every caller
already surfaces a rejection to the user.
The ResizeObserver fallback polls instead of returning a no-op. Doing nothing
there would restore the original bug on those browsers and do it without an
error, which is the worst way for a fix to be missing.
Signup had no bot protection. The account itself was never the prize — GoTrue
refuses a session until the address is confirmed, and 0 of the 2 unconfirmed
accounts on record ever signed in. The prize is our mail: /signup and /recover
send to whatever address the caller types, so a stranger could make
pezkuwichain.io mail thousands of people who never asked for it. Sending
reputation takes months to rebuild and every product on the domain shares it.
Rate limiting alone was not enough. GoTrue defaults to 30 emails/hour, which is
720 a day to attacker-chosen recipients; tightening it to 15 halves the number
without changing what is possible.
Verification runs inside GoTrue, not here. It POSTs to siteverify with the
widget secret before /signup, /recover, /resend, /magiclink, /otp and /token
with grant_type=password. Adding our own check in the frontend or a proxy would
protect nothing, because /auth/v1/signup is reachable directly.
One shared widget in execute mode rather than one per form: some of these are
buttons with nowhere to put a checkbox, and interaction-only appearance keeps
the challenge invisible unless Cloudflare asks for one. Tokens are single-use,
so each call renders fresh and tears down the previous widget.
Six call sites, matching the six protected endpoints — password login included,
since /token with grant_type=password is enforced. Refresh-token grants are
exempt, so open sessions are unaffected.
Site key travels as a build arg like the WalletConnect id: it is public by
construction, visible in the bundle. Only the secret is privileged and it lives
in GoTrue's env on the auth host.
Ships ahead of enforcement. Turning captcha on server-side before the frontend
sends tokens would break every login and signup in the same instant.
2FA could be enabled but was never enforced. TwoFactorSetup wrote a secret and
backup codes, and login never checked any of it — TwoFactorVerify existed as a
component referenced from nowhere. Turning 2FA on protected nothing.
Optional, as decided: a user without 2FA is never blocked, and a failure while
checking is treated as "not required" so a transient error cannot become a login
wall for people who never enabled it.
The challenge is enforced in ProtectedRoute rather than only after the password
form, so a deep link or a restored tab cannot walk straight past it. Telegram
sessions are exempt — they authenticate through signed initData, so there is no
password to second-factor.
Recovery is by backup code, which the verify endpoint already supported and
consumes on use. Cancelling the challenge signs the user out; leaving them signed
in but unchallenged would defeat the point of asking.
Verification is remembered in sessionStorage, not localStorage: 2FA is asked once
per browser session and again after the tab closes. Persisting it would mean
being challenged once per device, ever.
Scope, stated plainly rather than implied: this is a client-side gate. It stops
someone holding the password from reaching the app in a browser, which is what
2FA is for here. It is not server-side enforcement — privileged operations still
verify authority independently (wallet signature for admin actions, JWT for an
account's own data), and those remain what actually protects funds and state.
The function was never used and carried a real hole while sitting there.
`send` took an email address from the request body, did not check who was
calling, and returned the verification token in the response — with a
`// Remove in production` comment on that line. Anyone could mint a token for
another account's address and immediately use it. `Access-Control-Allow-Origin`
was `*`.
It was never reachable in practice: nothing calls `send`, so no token was ever
created, so `verify` could never succeed. The database confirms it —
`email_verification_tokens` has zero rows, ever.
Real email verification runs through Supabase Auth, not this function.
EmailVerification.tsx handles `type=signup` / `email_change` via the URL hash and
`getSession()`, and Dashboard.tsx resends through `auth.resend()`. Those paths
are untouched. The `?token=` branch that called this function was dead code
fed by a token nothing produced, so it goes with it.
Removing it also took the `verifying` state with it: `setVerifying` had no
remaining callers, so the spinner could never render and every `!verifying`
guard was permanently true — conditions that read as if the value could vary.
Fixing this properly instead of deleting it would mean building the email
delivery the function never had ("In production, send email via email service"),
duplicating what Supabase Auth already does correctly.
Brings the remaining four locales up to the same coverage as en and tr: the 14
multisigOps strings, wallet.multisigOperations, and the corrected withdrawal
wording.
The withdrawal strings were not merely missing here. bridge.withdrawAddress
existed in all four and still described the field as a bank account or crypto
address the user types in, and withdrawSuccess still said USDT would be sent
after a delay. Neither is true: the destination is derived from the connected
account, and burned wUSDT is only released once the multisig signers approve.
Because the keys existed, fallbackLng never applied and speakers of these four
languages were reading incorrect instructions about their own funds.
Terminology follows each file's existing vocabulary rather than inventing new
terms — kmr Îmze/Berîk, ckb واژوو/جزدان, ar توقيع/المحفظة, fa امضا/کیف پول.
The two Kurdish variants still want a native speaker's read before we treat
them as final.
t.me/pezkuwichain and twitter.com/pezkuwichain are both retired handles. The
canonical set is t.me/kurdishmedya for the channel and x.com/bizinikiwi for X.
Adds the entry point for the operations screen the multisigOps strings were
written for. The button only renders when the connected account is one of the
five USDT bridge signatories; that check is for discoverability only, the route
stays gated by ProtectedRoute.
MultisigOperations.tsx and MultisigOperationsPage.tsx are already in the tree and
read multisigOps.* plus wallet.multisigOperations, but no locale defined them, so
the screen rendered raw key names.
The bridge withdrawal copy was also wrong about what happens: the destination is
derived from the connected account rather than typed in, and burned wUSDT is not
released until the multisig signers approve. Both strings now say that.
ar, ckb, fa and kmr still carry the old withdrawal wording. They are not left to
fall back to English because the key exists there — they need translations from a
native speaker, tracked separately.
Round 2 — fixes the CRITICAL discovered during round 1 (as severe as the withdrawal
BOLA): lock_/release_/refund_escrow_internal had default PUBLIC EXECUTE and the client
called release_escrow_internal directly with the anon key, so anyone could drain any
victim's LOCKED balance.
- release_escrow_internal now reachable only via new signed confirm-payment edge
function: verifies seller wallet signature (raw + <Bytes>), asserts the wallet owns
the seller identity, derived user_id == trade.seller_id, trade == payment_sent,
single-use nonce, then release runs with the service role. confirmPaymentReceived
now invokes confirm-payment (no more anon rpc).
- lock_escrow_internal now behind new signed lock-escrow edge function (a caller can
only lock its own balance; stops griefing a victim's balance). Removed a zero-amount
no-op lock call.
- Migration 20260725030000: REVOKE EXECUTE on lock_/release_/refund_escrow_internal
from PUBLIC/anon/authenticated; GRANT to service_role only. refund has no direct
client caller (service-role + admin_resolve_dispute only).
- DEPLOY_RUNBOOK.md consolidates the ordered migrations, edge functions and secrets
for both security rounds. Migration 20260725030000 must ship WITH the two new edge
functions + frontend or offer-create/payment-release break.
Remaining (non-fund, Round 2+ follow-up): read RPCs still key on a non-secret user_id
(binding to a signed session would force a sign-prompt on every passive balance read —
deferred); p2p_fiat_offers/trades/messages retain USING(true) (status labels move no
funds now that all escrow movement is service-role-gated).
Critical/high audit remediation on the custodial P2P ledger. Auth model is
wallet-based; identity (citizen/visa) is cryptographically bound to a wallet
(People Chain tiki.citizenNft / active p2p_visa), which enables a correct fix.
- Withdrawal BOLA (CRITICAL): process-withdraw now requires a wallet-SIGNED
challenge; server verifies signature (raw + <Bytes> forms), asserts the signer
OWNS the identity, consumes a single-use nonce (replay), and derives user_id
server-side — the client-supplied user_id is ignored. process-withdrawal (batch)
now requires the service-role key (was: any bearer, incl. public anon key).
request_withdraw is REVOKEd from anon/authenticated + service-role guarded.
- Financial RLS (HIGH): drop the blanket USING(true) SELECT on user_internal_balances,
p2p_balance_transactions, p2p_deposit_withdraw_requests; lock p2p_user_payment_methods
(IBAN PII) + p2p_fiat_disputes UPDATE to service_role; legitimate reads move behind
scoped SECURITY DEFINER RPCs.
- Deposit integrity: verify-deposit now binds the on-chain sender to identity ownership
before crediting.
- Admin dispute (CRITICAL fund-logic): DisputeResolutionPanel relabeled trades without
moving escrow. New admin-signed resolve-dispute function + admin_resolve_dispute RPC
moves escrow (release/refund/split) atomically with correct accounting (avoids the
double-count in the legacy resolve_p2p_dispute). Client isAdmin documented as cosmetic.
DEPLOY RUNBOOK (gated; owner runs): 1) apply migrations 20260225/20260725* in order;
2) deploy edge functions process-withdraw, process-withdrawal, verify-deposit, resolve-dispute;
3) set edge secrets PEOPLE_RPC_ENDPOINT (+ optional ADMIN_WALLETS); 4) ship frontend.
Migrations + functions + frontend must go together or the app breaks.
KNOWN RESIDUAL (Round 2 — as severe as the withdrawal BOLA): release_/lock_/refund_
escrow_internal still have PUBLIC EXECUTE and the client calls release_escrow_internal
directly with the anon key from confirmPaymentReceived -> an anon caller can drain any
victim's LOCKED balance. Fix = a wallet-signed confirm-payment edge function (same
pattern as withdrawals) before revoking PUBLIC execute. Not yet fixed.
Audit remediation (build/supply-chain/quality):
- Add `@pezkuwi/api-augment` import in main.tsx -> tsc errors 416->328 (Codec-typed
on-chain reads now augmented); add `typecheck` script + a non-blocking Typecheck CI
step (flip to blocking once errors reach 0).
- CI: `npm install` -> `npm ci` (lockfile enforced) in web + security-audit jobs; add a
Backend Indexer job (npm ci + node --check + non-blocking high/critical audit). No
live tests run in CI (they need a live chain) — a mockable CI test subset is a
follow-up.
- Vulns: bump dompurify ^3.4.12 + postcss ^8.5.23 (web prod highs cleared; only the
react-router v7 open-redirect remains, deferred as a breaking major); backend tar
^7.5.22 + ws ^8.21.1 (clears the critical tar advisory). Lockfiles regenerated.
- Prod build now drops console/debugger (vite esbuild.drop); NotificationBell realtime
subscription now returns cleanup + uses a per-user channel (fixes leak); ProfileSettings
language codes aligned to i18n config (ku-kurmanji/ku-sorani).
- Root package.json sdk-ui path env-driven (${SDK_UI_DIR}); generate-docs rustup path
resolved dynamically; Docker image license label MIT (matches LICENSE). Removed scratch
files (mimari.txt, _scratch_credit_serok.mjs).
Fund-logic hardening (audit remediation):
- TransferModal: source asset id/decimals from the canonical MINTABLE_ASSETS
(wUSDT=1000, wDOT=1001, wETH=1002, wBTC=1003) instead of the wrong hardcoded
ids (BTC=3 was the deprecated "Old USDT", ETH=4, DOT=5). Route every non-native
asset through the Asset Hub api; only native HEZ uses the relay balances pallet.
Removes the latent wrong-asset / wrong-chain transfer.
- Replace BigInt(parseFloat(x)*10**dec) with string-based parseTokenInput in
TransferModal + 3 DEX init modals; BigInt(float) threw RangeError on common
fractional amounts (e.g. 100.3 HEZ), breaking sends and pool/bridge init.
- Presale.contribute now resolves a signer via getSigner and passes { signer }
to signAndSend (was signer-less -> broken for all users).
- presale.ts contribute/refund/claimVested now reject on dropped/invalid/usurped/
retracted tx states instead of hanging the UI forever.
Routing: footer "Explorer"/"Validators" pointed to non-existent /network (4 spots
in AppLayout + LandingPageDesktop) and Explorer/Forum quick-links pointed to bare
/governance (no route) -> all hit the 404 catch-all. Repointed to /explorer and
/governance/assembly; footer "Vote" /-> /elections.
Security (stored XSS): DiscussionThread.parseMarkdown did regex->HTML then
dangerouslySetInnerHTML with no escaping/sanitizer, so <img onerror>/javascript:
links in user comments executed. Now escape HTML first, then DOMPurify-sanitize the
output to a safe tag/attr allow-list with http(s)-only hrefs.
- calculateMultisigAddress was completely broken (hex-decoded an SS58 string
and never hashed the preimage) - fixed via @pezkuwi/util-crypto's real
encodeMultiAddress/createKeyMulti, verified against the actual known
multisig address on-chain.
- ReservesDashboardPage had stale Noter/Berdevk addresses that don't match
the real signers - centralized as BRIDGE_MULTISIG_SPECIFIC_ADDRESSES.
- USDTBridge withdrawal called assets.burn directly as a single-signer
extrinsic (always fails - only the multisig is Admin) while only
checking status.isFinalized (a failed dispatch is still finalized, so it
silently did nothing) - replaced with the correct transfer-to-custody
flow the relayer actually watches for.
- New MultisigOperationsPage (/multisig/pending) lists pending calls from
real Multisig.Multisigs storage and lets any of the 5 signers
approve/reject with their own wallet extension.
- New standalone sign/ app (deployed separately at
pezbridge-sign.pex.mom) - a dedicated, gated signing portal for the same
operations, so signing isn't dependent on this app alone.
Replace the placeholder Assembly page with a professional on-chain governance
landing (animated 201-seat hemicycle, live chain ticker, blocs, sessions, active
votes, features). Content is fully localized into the 6 standard app languages
(en/tr/kmr/ckb/fa/ar) with RTL for ckb/fa/ar; each language variant is lazy-loaded.
PasswordReset called a non-existent 'password-reset' edge function, so
users could never reset a forgotten password. Switch to Supabase Auth's
built-in recovery: resetPasswordForEmail() to request the email, and
updateUser({password}) within the PASSWORD_RECOVERY session to set the
new password. Generic success message (no account enumeration); sign out
after reset to force clean re-login.
Rebuild the /identity ID card and passport as flip cards rendered fully in
code (HTML/CSS/SVG) with each citizen's real data:
- e-ID: holographic light card, front (BÊ KURDISTAN JÎYANE NÎNE, photo, gold
chip, bilingual fields NAV/NAME · PASNAV/SURNAME · DATE OF BIRTH ·
NATIONALITY KURDISTANÎ/KURDISH · ID NUMBER, faux-QR, e-ID: KOMARA KURDISTAN
footer) + back (DIGITAL KURDISTAN STATE: ID number, digital wallet ID,
biometric status, valid-until, gov services, support; authority seal).
- Passport: navy cover with the gold ram (mouflon) emblem + Sorani/English
titles, flips to a holographic data page (flag stripe, bilingual fields,
photo, ICAO-style MRZ).
- Adds a Surname field and pulls the connected wallet address for the e-ID
back. Document labels are the official bilingual set (not UI-translated);
data stays device-local. Tap ↻ to flip.
The Finance 'P2P/Buy-Sell' tile label was the hardcoded Latin string
'P2P/Buy-Sell' in all six locales, so it stayed untranslated (English) when
switching to Sorani/Farsi/Arabic/Turkish/Kurmancî. Keep the universal 'P2P'
token and translate the buy-sell part (TR Al-Sat, KMR Kirîn-Firotin,
CKB کڕین-فرۆشتن, FA خرید-فروش, AR بيع-شراء).
The Finance section tile label becomes 'P2P/Buy-Sell'. In the P2P dashboard's
top nav, next to the Messages icon, a Visa card (CreditCard) action 'Buy with
Visa' opens buy-sell.pezkiwi.app in a new tab. Adds p2pNav.buyVisa in all 6
locales.
In the Social section of both the desktop app grid and the mobile home, the Help
tile becomes a Loto tile with the Newroz flame logo (loto-icon.svg) that opens
loto.pex.mom in a new tab. Adds the mobile.app.loto i18n key in all 6 locales.
Help remains reachable from the landing page.
- KurdMedia: DKS Rojname channel now links to the Dijital Kurdistan News site
- Social Events item opens the Kurdish TTS app (was coming-soon/locked)
- applies across mobile drawer, mobile home and desktop landing pallets
* fix(ci): unblock deploy pipeline (audit gate + orphan submodule)
The Quality Gate & Deploy pipeline was failing at security-audit
(npm audit --audit-level=high), which blocks telegram-gate and the
whole deploy chain — that is why production was serving a stale bundle.
- npm audit fix (no --force, lockfile only): clears the critical vitest
advisory (GHSA-5xrq-8626-4rwp) and the high elliptic one; only low-
severity items remain, so 'npm audit --audit-level=high' now exits 0.
- Remove the orphaned 'exchange' gitlink: it is an empty submodule
pointer with no .gitmodules mapping, which made git print
'fatal: no submodule mapping found' during checkout.
Verified: lint, test (32 passed), and vite build all pass; audit gate
is green. No package.json changes.
* feat(web): PEZ-20 badge on PEZ and USDT balance cards
Add a small reusable Pez20Badge pill next to the PEZ and USDT tokens in
the wallet balance view, linking to the Token Standards docs. These are
fungible assets on Asset Hub, i.e. the PEZ-20 standard — this gives users
the familiar ERC-20-style mental model at a glance.
Additive only: no labels removed, native HEZ is intentionally not badged
(it is the native/gas token, not a PEZ-20 asset).
Staking migrated to Asset Hub (AHM), but the landing page still read
nominators from the relay (api.query.staking.counterForNominators),
which is now empty there — so the count showed '—'. Collators were read
from collatorSelection.candidates (empty; collators are invulnerables)
and only on Asset Hub, missing the People chain set.
- Nominators: query Asset Hub staking.counterForNominators (verified 30).
- Collators: count collatorSelection.invulnerables on both Asset Hub and
People chain (2 + 2), tracked per-chain and summed.
- NetworkStats.tsx already used the correct sources; this aligns the
landing page with it.
B2B button (/bereketli SSO interstitial): if there is no Supabase session
or the token exchange fails, redirect to https://bereketli.pezkiwi.app
instead of stranding the user on app.pezkuwichain.io/bereketli. (The
backend CORS allowlist was also missing app.pezkuwichain.io; fixed
server-side so the SSO exchange itself now succeeds.)
- Wrap web3Enable() with Promise.race against a 20-second timeout
- On timeout: show descriptive error explaining the popup may be blocked
- Surface actual error messages (incl. timeout) instead of generic 'Failed to connect wallet'
- Both auto-restore and manual connect button now fail fast instead of hanging
- Extension button now shows 'Approve in extension...' spinner while web3Enable waits
- Add generic error fallback for errors not matching 'authorize'/'not found' patterns
- Replace 'Coming soon on Play Store' with real Play Store download link (io.pezkuwichain.wallet)
- WalletConnectModal mobile hint now links directly to Play Store
- Updated in all 6 locales: en, tr, ar, fa, kmr, ckb
- Remove ArrowRightLeft icon from desktop nav Trading dropdown button
- Bottom tab bar: add max-w-md mx-auto (centered) and bump z-index to z-50 to match MobileHomeLayout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add body content sections (HeroSection, NetworkStats, TrustScoreCalculator, ChainSpecs, RewardDistribution) after section grid
- Update section cards with distinct gradient header colors per category (Finance/green, Governance/purple, Social/blue, Education/orange)
- Fix bottom tab bar to be full-width (removed max-w-md mx-auto)
- Adjust role/score cards background to bg-gray-800/70 for contrast against main bg
- Add bodyOnly prop to LandingPageDesktop (non-breaking, unused)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add isApiInitializing state (true during WS connect, false on ready/fail)
- Add isApiReadyRef for closure-safe polling in connectWalletConnect
- connectWalletConnect now waits up to 30s for API instead of throwing immediately
- WalletModal connect buttons disabled + show spinner while blockchain is initializing
TelegramConnect: query tg_users instead of users, resolve visa UUID
from p2p_visa table and store as p2p_user_id for cross-platform P2P.
P2PIdentityContext: when citizen resolves their UUID, backfill
tg_users.p2p_user_id if their wallet is linked to a Telegram account.
Replaces the buggy useEffect that derived citizen number from wallet
address (and re-triggered whenever user cleared the field) with a clean
sync from DashboardContext's NFT-derived citizenNumber. Field is read-only
when NFT data is present, preventing the refill loop entirely.
- Use PezkuwiExchange.png logo instead of emoji for exchange app icon
- External link opens exchange.pezkuwichain.io in new tab (noopener)
- No auth required (exchange is publicly accessible)
- Added imgIcon and href fields to AppItem interface