Commit Graph
954 Commits
Author SHA1 Message Date
dependabot[bot] 2c6f3d00f8 ci: bump the actions group across 1 directory with 11 updates
Bumps the actions group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4.4.0` | `7.0.1` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `3.38.3` | `4.38.2` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `3.38.3` | `4.38.2` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `3.38.3` | `4.38.2` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.1` |
| [actions/setup-node](https://github.com/actions/setup-node) | `4.4.0` | `7.0.0` |
| [actions/cache](https://github.com/actions/cache) | `4.3.0` | `6.1.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.12.0` | `4.4.1` |
| [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `3.10.1` | `4.1.2` |
| [docker/login-action](https://github.com/docker/login-action) | `3.7.0` | `4.6.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `6.19.2` | `7.4.0` |



Updates `actions/checkout` from 4.4.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/11d5960a326750d5838078e36cf38b85af677262...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `github/codeql-action/init` from 3.38.3 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9f759ee644a3e7c15c1390abf49868036c00067b...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `github/codeql-action/autobuild` from 3.38.3 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9f759ee644a3e7c15c1390abf49868036c00067b...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `github/codeql-action/analyze` from 3.38.3 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9f759ee644a3e7c15c1390abf49868036c00067b...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `actions/upload-artifact` from 4.6.2 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `actions/setup-node` from 4.4.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...820762786026740c76f36085b0efc47a31fe5020)

Updates `actions/cache` from 4.3.0 to 6.1.0
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/0057852bfaa89a56745cba8c7296529d2fc39830...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)

Updates `docker/setup-buildx-action` from 3.12.0 to 4.4.1
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...f87e5991a6d7451dcb8d9637bfbc97413f497069)

Updates `sigstore/cosign-installer` from 3.10.1 to 4.1.2
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/7e8b541eb2e61bf99390e1afd4be13a184e9ebc5...6f9f17788090df1f26f669e9d70d6ae9567deba6)

Updates `docker/login-action` from 3.7.0 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...dbcb813823bdd20940b903addbd779551569679f)

Updates `docker/build-push-action` from 6.19.2 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-09 03:24:27 +00:00
SatoshiQaziMuhammed c57e7e0ef4 web: make the typecheck a real gate, held to a ratchet (#54)
The typecheck ran with continue-on-error, so it could not fail, and it did
not see much of the code it was meant to check: shared/ resolved none of its
imports (61 TS2307), which hid every type error behind them.

- tsconfig.app.json resolves packages that shared/ imports from web's
  node_modules (types first), and @pezkuwi/utils subpaths as vite does.
  Unresolved imports: 61 -> 0. The real errors behind them are now visible:
  353 in total, most in production code with little test cover.
- Fixing those in one change would be a large edit to that code. Instead
  ops/typecheck-ratchet.py records them per file and error code in
  ops/typecheck-baseline.txt and fails when any file gains one, or when
  errors were fixed without lowering the baseline in the same change.
  Line numbers are not part of the signature, so unrelated edits do not
  trip it. Checked: a new error in App.tsx fails (exit 1); an unlowered
  baseline fails (exit 1); the current tree passes.

The CI step runs the ratchet instead of the non-blocking typecheck.
2026-10-09 07:18:26 +04:00
SatoshiQaziMuhammed b6c2ce3470 Node 24, and stop pointing at the mobile app that left the repository (#53)
mobile/ was moved out in April (18d41743), but the root package.json still
ran it: `npm run build`, `lint`, `test` and `dev` all called into a missing
directory and failed. The scripts now cover what is here (web, sign,
backend), concurrently is gone with the only command that needed it, and the
root lockfile shrinks to nothing.

The README described the mobile app and not the signing portal; it now lists
the real layout (web, sign, backend, shared, ops).

Node 20 is past its end of life. CI and both images move to Node 24; the
backend's sqlite3 native module builds and its tests pass on 24.
2026-10-09 07:15:39 +04:00
SatoshiQaziMuhammed 44f8e3d418 ci: let Dependabot keep the pinned workflow actions current (#52)
The actions are pinned to commit SHAs, and nothing proposes updates to
them: the Node 20 runtime several of them still declare is already being
forced to Node 24, with a warning on every run. Dependabot now checks them
monthly and groups all action updates into one pull request.
2026-10-09 07:12:54 +04:00
SatoshiQaziMuhammed 3ba40d4e52 Deploy over pinned, single-purpose keys from GitHub-hosted runners (#51)
- ops: deploy scripts the CI keys are pinned to
- ci: deploy over pinned, single-purpose keys from GitHub-hosted runners
- ops: tell shellcheck the test's variables are read through eval
2026-10-09 04:11:51 +04:00
SatoshiQaziMuhammed b09af9bfbd Move to the fixed crypto stack and patched dependencies; run quality CI on hosted runners (#47)
- Move backend and root transitive deps to patched versions
- Upgrade web to react-router 7.18 and patched vitest/dompurify
- Move the sign app to vite 6.4.3
- Depend on the fixed crypto stack in web, sign and backend
- sign: typecheck the shared code it compiles
- ci: run the quality jobs on GitHub-hosted runners, and build sign
- Move to @pezkuwi/api 16.5.39
- ci: run CodeQL for JavaScript/TypeScript on a GitHub-hosted runner
2026-10-09 01:34:14 +04:00
SatoshiQaziMuhammed 74db90a5d4 Send the Rewshenbir button to rewshenbir.pezkiwi.app (#38)
* 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.
2026-10-01 15:53:08 +04:00
SatoshiQaziMuhammed 772cb72c73 Serve the whitepaper from one place, and stop cloning what cannot be cloned
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.
2026-09-04 03:25:35 +04:00
SatoshiQaziMuhammed b289bbc0ae ops: make the drift check see rulesets, not just branch protection (#36)
The check reported branch protection fully green while a repository ruleset on
the same branch required two status checks that no workflow produces —
'Build, Lint & Test' and 'All checks passed', names left behind when the jobs
were renamed. Nothing could merge without --admin, and the report said
everything was fine.

That is the worst shape a check can take: confident and blind. A branch can be
governed by classic protection and a ruleset at once, independently, and the
stricter wins. Reading one and calling it the answer is how a broken gate stays
invisible for months while the team learns to route around it.

--check now reads active rulesets too, and compares each required check against
what recent pull requests actually report. A rule waiting on a check that never
arrives does not slow a branch — it closes it.

Sampling uses recent PR heads rather than main's head, because the aggregate
gate only runs on pull_request; sampling main would report it as never seen and
manufacture the opposite false alarm.
2026-08-01 21:04:26 -07:00
SatoshiQaziMuhammed 39b71a428e ci: make the approval notification able to fail (#35)
The step piped Telegram's reply to /dev/null and then printed 'Approver
notified' unconditionally. A stale token, a wrong chat id, a rejected message —
all of them ended with a green step and an approver who never heard about the
deploy.

That is not hypothetical. On 2026-08-01 run 30710528288 sat in 'waiting' with
the log reading 'Approver notified; deployment waits on the production
environment', and no message had arrived. Nothing in the run said otherwise,
so the first signal was a person asking why the deploy had not moved.

The reply is now checked and a rejection fails the job. Exchange's equivalent
step already did this; pwap's did not, because I wrote it without copying the
pattern that was already there and working.
2026-08-01 19:39:11 -07:00
SatoshiQaziMuhammed 78fd8b0514 ops: stop requiring an approval that nobody can give (#34)
The rule said one approving review. On a four-person team where the author is
usually the only person awake, that meant every merge went through
`gh pr merge --admin` — so the requirement was not producing review, it was
producing a habit of bypassing the branch rule. A control everyone routes around
is worse than no control: it still shows up green in an audit.

Zero required approvals, and the rest of the protection stays exactly as it was.
Nothing lands on main without a PR; nothing merges with red CI. That is the part
a team this size actually gets value from, and it costs no waiting.

What was being protected here was never code review. It was that nothing reaches
production unnoticed, and that funds cannot move on one person's say-so. Both
live elsewhere: production waits on the environment gate, and fund movement is
going behind on-chain multisig — where a stolen account cannot forge a second
signature the way a collected token can forge a second approval.

Drift check passes against live settings.
2026-08-01 10:31:51 -07:00
SatoshiQaziMuhammed 338472629d fix(auth): put the captcha challenge where the user is looking (#33)
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.
2026-08-01 06:16:44 -07:00
SatoshiQaziMuhammed c26eeee12c feat(auth): require a captcha on the endpoints that send mail (#32)
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.
2026-08-01 03:45:51 -07:00
SatoshiQaziMuhammed 35884cb8c9 ci: put the approve button back in Telegram (#31)
Moving approval to a GitHub Environment fixed the failure it was meant to fix,
and quietly broke something that worked. The old gate polled a file for 30
minutes holding a runner; on 2026-07-30 the window expired unseen and cancelled
a deploy. What replaced it holds the run for 30 days on no runner and records
who approved which SHA — but only answers to a browser. For a single approver
that means being at a PC for a deploy to move.

The notification now carries Approve and Reject buttons that answer the
environment's pending deployment through @pexsecBot. Nothing about the
enforcement changes: the protection rule still holds the deploy, and the bot
only presses the button as the reviewer. If the bot, its host, or its token is
gone, a release waits — none of that lets an unapproved deploy through. The
GitHub link stays as the fallback.

Bot side: pezkuwichain/pexsec-bot (new private repo — it had been an
unversioned copy on NEW-10 with three .bak files beside it).
2026-08-01 02:30:20 -07:00
SatoshiQaziMuhammed 7c9fbfb4b2 ci: one approval per deploy, not two (#30)
All four deploy jobs carried `environment: production`, which reads as the
stricter design — GitHub holds each one until approved. It does not behave that
way. Approval requests are batched by eligibility, and these jobs are never
eligible together: deploy-supabase waits only on the notification while the rest
wait on image builds. So a run asks twice, minutes apart, and nothing announces
the second round.

That happened on 2026-07-31. The first approval shipped the Supabase functions
and migrations; the run went back to waiting and stayed there. For as long as it
sat, the schema had moved and the app serving it had not.

Deployment protection now lives on a single `approve-deploy` job that the four
deploy jobs depend on. One approval releases the whole set.

That trades GitHub's own per-job enforcement for a dependency edge, and an edge
can be dropped by a later edit. ops/check-deploy-gate.py makes it a guarantee
again: it fails CI if a deploy job does not depend on the gate, if a second job
declares the environment and brings the split back, or if a job uses always()
without asserting the gate succeeded — which looks correct and silently
un-holds it. Verified against all three by mutating the workflow.

Also documented in ops/README.md: a run waiting on approval cannot be stopped
with `gh run cancel`; its pending deployment must be rejected. And that
bump-version still pushes to main before the gate — left alone, but written
down rather than left to be rediscovered.
2026-08-01 00:55:13 -07:00
SatoshiQaziMuhammed f9d784f3de Merge pull request #29 from pezkuwichain/feat/2fa-login-gate
feat(auth): actually ask for 2FA at login
2026-07-31 23:07:22 -07:00
Satoshi Qazi Muhammed e8dd9f6c99 feat(auth): actually ask for 2FA at login
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.
2026-07-31 22:02:40 -07:00
SatoshiQaziMuhammed a3fde0dc9d Merge pull request #28 from pezkuwichain/db/baseline-schema
db: replace the migration history with a baseline of the live schema
2026-07-31 20:26:56 -07:00
SatoshiQaziMuhammed cec1dcf9be Merge pull request #27 from pezkuwichain/fix/remove-dead-email-verification
fix(security): remove the dead email-verification function
2026-07-31 20:26:51 -07:00
Satoshi Qazi Muhammed e18ba679be db: replace the migration history with a baseline of the live schema
The migrations did not describe this database, and could not be made to.

Three findings, in the order they surfaced:

  * 25 functions declared across six migrations — all recorded as applied — did
    not exist. Their tables did. Two were reached by the app, so merchant tier
    upgrades and post-trade reputation updates had been quietly dead. This only
    came to light because a user hit "Could not find the function
    public.upsert_user_profile(...)" while toggling a notification setting.

  * admin_roles has three conflicting definitions across the set and production
    matches none of them. 001 says (id, user_id, role, granted_by, granted_at),
    COMBINED says (user_id, role, created_at), production has
    (id, user_id, role, permissions, created_at, updated_at).

  * Applied to an empty database, five migrations fail. The legacy 0NN filenames
    sort before the 14-digit timestamps they depend on — "013" < "20241117054600"
    — so 013 runs before the migration creating the table it alters. The set
    could never have been replayed from scratch.

So it could not be tested, could not rebuild the database, and did not match what
was running. Widening or patching it would have been dressing up a history that
was already fiction.

The baseline is a pg_dump of the live public schema, which matches production by
construction. Privileges are included deliberately: the REVOKEs on
lock_escrow_internal, release_escrow_internal, refund_escrow_internal and
request_withdraw are the 20260725030000 hardening, and dropping them would hand
fund movement back to anon.

Verified step by step against production before committing:
  - applies to an empty database cleanly, after three real obstacles were fixed
    (extensions live in the `extensions` schema, supabase_admin membership,
    platform-level ALTER DEFAULT PRIVILEGES that cannot apply outside Supabase)
  - produces 83 tables / 39 functions / 222 indexes / 360 policies — identical
    counts to production
  - recorded as applied in supabase_migrations without touching the 37 existing
    rows, then dry-run confirmed "up to date — no pending migrations", so the
    next deploy will not try to replay it over live tables
  - drift check now reports "every declared function is present"; the known-gaps
    list drops from 22 entries to zero

The old files move to migrations/archive/ rather than being deleted — they are
the only record of why parts of this look the way they do. Their README says
plainly not to run them, and why.

CI now applies migrations to an empty Postgres on every PR. That check was
impossible while the old set was the starting point; it is the thing that stops
this class of drift from being discovered by a user again.
2026-07-31 18:49:59 -07:00
Satoshi Qazi Muhammed c0e1326c4b fix(security): remove the dead email-verification function
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.
2026-07-31 11:05:01 -07:00
SatoshiQaziMuhammed b20cfc84ef Merge pull request #26 from pezkuwichain/ci/extend-deploy-approval-window
ci: gate deploys on a GitHub Environment instead of a hand-rolled wait loop
2026-07-31 07:27:48 -07:00
Satoshi Qazi Muhammed 7482421f3d ops: capture branch protection and the deploy environment as code
Both were configured by hand through the GitHub API today. Nothing in the repo
reflected them, so they were invisible here, unversioned, and removable without
a trace — which undercuts their purpose, since they exist so that nothing reaches
main or production unreviewed.

apply-repo-settings.sh is now the source of truth for both, and idempotent: each
call PUTs the full desired state rather than stacking changes. --check reports
drift without touching anything, which is what makes this useful later — a
setting silently relaxed is otherwise only discovered when it matters.

Documented alongside it: why CI Gate ✅ is the only required check (it is an
aggregate that already inspects the others, and listing each job would mean a
rename quietly drops a requirement), and why enforce_admins stays false (an
incident needs an escape hatch — rare, visible, recorded).

This is the same class of problem as the migrations fixed earlier today: state
that only exists because someone once clicked something, with no record of what
or why.
2026-07-31 02:07:21 -07:00
Satoshi Qazi Muhammed e4abb38757 ci: gate deploys on a GitHub Environment instead of a hand-rolled wait loop
Approval was implemented as a job that polled /tmp/pexsec-gates for 30 minutes
while holding a pwap-runner slot. On 2026-07-30 the request went out at 22:14,
was not seen in time, and at 22:44 the gate cancelled the deploy — #25 stayed
merged but never reached the database, and the only trace was a failed job.

Widening that timeout was the wrong fix, so this replaces the mechanism.

Deploys now gate on the `production` environment, which has SatoshiQaziMuhammed
as a required reviewer and is restricted to protected branches. GitHub holds the
run until it is approved:

  - no runner is occupied while waiting (the loop burned one for the full window)
  - the window is 30 days, not minutes, so a missed notification costs nothing
  - the approval is recorded — who approved which SHA, in deployment history
  - approval state lives outside the workflow, so a runner restart cannot lose it

telegram-gate becomes notify-deploy-pending: it sends one message and exits in
seconds, and cannot block anything. The message now links to the run rather than
carrying Approve/Cancel buttons, since approval happens in GitHub.

Only the four jobs that touch production carry the environment. Image builds and
the version bump stay ungated and run in parallel with the wait; nothing they
produce is user-visible until a deploy job runs.
2026-07-30 18:36:12 -07:00
Satoshi Qazi Muhammed 6b368d9735 ci: widen the deploy approval window from 30 minutes to 6 hours
#25 was merged, the approval request went out at 22:14, nobody saw it in time,
and at 22:44 the gate cancelled the deploy. Nothing shipped and the only trace
was a failed job — the repair migration sat merged but unapplied.

Thirty minutes assumes the approver is at their phone when the request lands. A
meeting or a night's sleep loses the deploy, and the failure mode is silent
unless someone goes looking at Actions.

Six hours, with the deadline now stated in the Telegram message itself so the
approver knows what they are working against. Job timeout-minutes is set to 370
explicitly: without it the job relies on GitHub's 6h default, which would kill
the job at almost exactly the moment the script is still waiting.

Worth being honest about the tradeoff: this job holds a pwap-runner slot for the
entire wait, so six hours of waiting is six hours that runner cannot do anything
else. The right fix is a GitHub Environment with required reviewers — that holds
no runner while it waits and allows up to 30 days. This loop is a hand-rolled
stand-in for that mechanism; widening it buys time but does not remove the cost.
2026-07-30 16:23:38 -07:00
SatoshiQaziMuhammed c4b22d0a14 Merge pull request #25 from pezkuwichain/fix/repair-missing-db-functions
fix(db): repair functions their migrations recorded but never created
2026-07-30 14:56:21 -07:00
Satoshi Qazi Muhammed 658c99b9bb fix(db): repair functions their migrations recorded but never created
A user hit "Could not find the function public.upsert_user_profile(...) in the
schema cache" when toggling push notifications. The function was missing, and so
were the profiles columns it writes — while schema_migrations listed both 002 and
004 as applied.

That pattern turned out to be widespread. Comparing every function declared
across the migrations against the live database: 25 are missing, from six
migrations all recorded as applied. Their tables exist; only the function bodies
are absent. Those files carry "Run this in Supabase SQL Editor" headers, so they
were pasted in by hand before apply-migrations.sh existed and a run that stopped
partway was still recorded. The runner has skipped them ever since, which is why
this stayed invisible until it surfaced as a user-facing error.

apply-migrations.sh is not at fault: it wraps each migration and its tracking row
in one transaction with ON_ERROR_STOP, so it cannot half-record anything. It
inherited a dirty history.

Three of the 25 are actually reached by the app, so those are repaired here:

  apply_for_tier_upgrade   MerchantApplication.tsx:213 - tier upgrades were dead
  check_tier_eligibility   called by the above
  update_p2p_reputation    shared/lib/p2p-fiat.ts:803 - reputation never updated

This is forward-only repair, not a replay of the source files. Replaying them
would abort on their bare CREATE POLICY/INDEX/TRIGGER statements now that the
tables exist, and 20241117054602 in particular would overwrite 016's newer
cancel_expired_trades with its own older definition.

Verified before writing: every table the three functions touch is present, and
the whole migration was run inside BEGIN/ROLLBACK against production — three
functions created, signatures matching what the callers pass, then rolled back
leaving nothing behind.

The remaining 22 stay missing on purpose and are listed in known-schema-gaps.txt.
Nothing calls them, and several are trigger bodies whose triggers were never
created either, so creating them would switch on behaviour that has never run.

Also adds a drift check to apply-migrations.sh: after applying, it compares
declared functions against the database and flags anything missing that is not in
the known-gaps list. Being recorded as applied is not proof of having been
applied, and that gap should never again be discovered by a user. The check
already earned itself — it caught update_p2p_reputation, which my own first pass
had missed to a regex that dropped digits from function names.

upsert_user_profile and the profiles notification columns were applied directly
to the database from their existing migrations (002, 004), which are fully
idempotent; no new migration was needed for them.
2026-07-30 09:16:01 -07:00
SatoshiQaziMuhammed e95907ec87 Merge pull request #24 from pezkuwichain/fix/function-authorization
fix(security): identify the caller from their token, not the request body
2026-07-30 04:25:26 -07:00
Satoshi Qazi Muhammed 0fe8c85dfe fix(security): identify the caller from their token, not the request body
two-factor-auth and notifications-manager run with the service role, so they
bypass RLS and whatever user id they act on is the entire authorisation
decision. Both read that id from the request body, and neither checked who was
calling. Access-Control-Allow-Origin was '*' on both.

Confirmed against the live endpoint: with only the public anon key and no user
session, from an arbitrary origin,

  POST /functions/v1/two-factor-auth {"action":"check","userId":"<any-uuid>"}
  -> 200 {"success":true,"enabled":false}

VERIFY_JWT is on and does not stop this, because the anon key is itself a valid
JWT and every browser has it. The same path reaches "disable", so anyone could
turn off another account's 2FA, and read, delete or forge their notifications.

The client already sends the signed-in user's token — functions.invoke puts the
session access token in the Authorization header — so the caller can simply be
read from it. Both functions now do that and ignore the body's userId. An
anon-key call carries no user, so it is rejected with 401.

create is the one action that may target someone else, because AdminPanel
notifies other users; that path now requires admin or super_admin in
admin_roles. CORS drops to the same allowlist the other functions here use.

No frontend change: the body may keep sending userId, it is simply not read.

Note: 2FA is not yet enforced at login — TwoFactorVerify is not referenced
anywhere and Login.tsx never checks it, so enrolling does not currently protect
sign-in. Tracked separately; this change is about the endpoints themselves.
2026-07-30 04:05:03 -07:00
SatoshiQaziMuhammed 46669aa72d Merge pull request #23 from pezkuwichain/feat/adopt-cloud-functions
feat(supabase): adopt the three functions left behind on the cloud project
2026-07-30 02:40:23 -07:00
Satoshi Qazi Muhammed dc8533d707 feat(supabase): adopt two-factor-auth, notifications-manager, email-verification
The web app calls these three from TwoFactorSetup, TwoFactorVerify,
NotificationBell, AdminPanel and EmailVerification, but this repo never defined
them and they are not deployed to the self-hosted host the app points at. They
were left behind on the cloud project when everything else moved, so every one
of those calls has been hitting a function that is not there — two-factor auth,
notifications and email verification have all been silently dead.

Recovered from the deployed bundles on the cloud project and added here so the
source is tracked and CI deploys them alongside the rest. The tables they need
(two_factor_auth, notifications, activity_logs, profiles,
email_verification_tokens) already exist on the self-hosted database, so this is
a code move only.

Registry on the host now lists all three under this project.

NOTE: two-factor-auth and notifications-manager take userId straight from the
request body while running with the service role, and neither checks the caller.
That is how they run on the cloud project today, so this change does not make it
worse, but it does mean anyone can disable another account's 2FA or read and
delete their notifications. Fixing it properly needs an admin-role check
alongside the JWT, because AdminPanel legitimately creates notifications for
other users. Tracked separately rather than folded into a move.
2026-07-30 02:17:29 -07:00
SatoshiQaziMuhammed 8cc3935b91 Merge pull request #22 from pezkuwichain/ci/backend-deploy-guard
ci(supabase): ownership gate for shared edge functions + complete 6-locale copy
2026-07-30 02:05:16 -07:00
Satoshi Qazi Muhammed 9e10eee977 Merge remote-tracking branch 'origin/main' into ci/backend-deploy-guard 2026-07-30 00:45:09 -07:00
Satoshi Qazi Muhammed 08fd9d2b34 i18n(ar,ckb,fa,kmr): complete multisig operations and bridge withdrawal copy
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.
2026-07-29 20:34:46 -07:00
Satoshi Qazi Muhammed cff5ae8032 fix(social): point KurdMedia links at the canonical accounts
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.
2026-07-29 20:26:00 -07:00
Satoshi Qazi Muhammed c117b023b1 feat(wallet): surface multisig operations shortcut to bridge signatories
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.
2026-07-29 20:26:00 -07:00
Satoshi Qazi Muhammed 88f1eb7245 chore(images): drop unused assets, recompress HEZ token logo
react-logo, partial-react-logo, hez-token, wallet and HEZ_Token_Logo_512 have no
remaining references anywhere in the tree (the wallet.png hits in
LandingPageDesktop are /ref-stepN-wallet.png under public/, different files).

HEZ_Token_Logo.png recompressed 5.2 MB -> 3.9 MB, same image.
2026-07-29 20:24:52 -07:00
Satoshi Qazi Muhammed ee384064c1 i18n(en,tr): add multisig operations strings, correct bridge withdrawal copy
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.
2026-07-29 20:24:51 -07:00
Satoshi Qazi Muhammed 30cca41d37 ci(supabase): deploy edge functions through the ownership gate
The deploy step rsynced this repo's whole functions tree into the shared
edge-runtime volume. The comment on that line acknowledged the volume was
shared, but additive-without---delete only protects names we do not also use:
this repo and pezkuwi-telegram-miniapp both ship a telegram-auth, so the
2026-06-28 deploy replaced theirs and broke that app's sign-in for a month.

Writes now go through supabase-deploy-functions on the host, which validates
every incoming directory against a registry of who owns which name and refuses
the whole deploy if any of them is not ours. Nothing this repo legitimately
deploys changes; the miniapp has moved its two colliding functions to a tgm-
namespace, so the two projects no longer address the same directories.

Registry: /opt/supabase-self-hosted/functions-registry.json
2026-07-29 20:22:03 -07:00
SatoshiQaziMuhammed bb0b7d4183 Merge pull request #21 from pezkuwichain/ci/backend-deploy-guard
ci: backend indexer deploy — self-contained config + host gate
2026-07-25 21:26:56 -07:00
Satoshi Qazi Muhammed 90f94df24b ci: make backend indexer deploy self-contained + config-gated
Two problems with the backend deploy, fixed together:

1. It errored on every main push with 'missing server host' because
   BACKEND_VPS_* wasn't set. A permanently-red job is a broken window —
   it trains people to ignore deploy alerts and hides real failures.
   Add a backend-cfg check job; deploy-backend now runs only when
   BACKEND_VPS_HOST is set and skips cleanly (neutral) otherwise,
   activating automatically once the host is provisioned.

2. It required a hand-placed /opt/pwap-indexer/.env on the host — a
   fragile out-of-band manual step. The deployed image runs only the
   indexer (src/index.js), which needs just WS_ENDPOINT beyond the fixed
   PORT/DB_PATH (no KYC/council service, so no seed/Supabase secret).
   Inject WS_ENDPOINT from the workflow (INDEXER_WS_ENDPOINT secret with a
   public default) via -e; drop the --env-file dependency. Config now lives
   in the pipeline, not a manual file on the box.
2026-07-25 21:08:07 -07:00
SatoshiQaziMuhammed 3d786f47d3 Merge pull request #20 from pezkuwichain/audit-remediation
security: P2P fund-custody audit remediation + backend CI/CD (proven-by-execution)
2026-07-25 18:30:22 -07:00
Satoshi Qazi Muhammed 85b2c48025 ci: automated Telegram-gated Supabase deploy (edge functions + migrations)
Adds a deploy-supabase job to the quality gate so the self-hosted Supabase
side ships the same way as the frontend/backend instead of by hand:

- gated by the existing telegram-gate (owner approves once, in Telegram)
- runs on main-push in parallel with deploy-app, so edge functions, DB
  migrations and the new frontend go live in one post-approval window
- rsyncs the edge-function dirs into the edge-runtime volume (hot-reloaded,
  no restart; --no-delete preserves the telegram-* functions)
- applies pending migrations via deploy/apply-migrations.sh: idempotent and
  transactional, tracked in supabase_migrations.schema_migrations, skips
  non-versioned files (COMBINED_*), never half-records a failed migration
- post-deploy health check verifies the fund-custody guards took effect
  (anon lost EXECUTE on the escrow/withdraw RPCs; both freeze triggers exist)
- Telegram alert on success/failure

Requires repo secrets SUPABASE_VPS_HOST/USER/SSH_KEY/PORT.
2026-07-25 17:53:31 -07:00
Satoshi Qazi Muhammed 193ef34c3e ci: exclude Deno supabase tests from vitest sweep
vitest's default include swept supabase/functions/**/__tests__ and
supabase/migrations/__tests__ — those are Deno tests (Deno.test + pglite)
that only run under `deno test` and fail under node/jsdom. Add supabase/**
to test.exclude so the web test run covers only the Vite app's own suites.
2026-07-25 16:50:13 -07:00
Satoshi Qazi Muhammed 5cf55aecbf ci: exclude Deno edge functions and migration tests from web eslint
The web ESLint config (browser/React rules: no-explicit-any, no-unused-vars)
was linting supabase/functions/** and supabase/migrations/** — Deno TypeScript
that legitimately uses `any` for the chain/Supabase surfaces and npm: imports.
These have their own tooling (deno check + deno test) which passes. Add both
dirs to ignores so `eslint .` covers only the Vite app it is configured for.
2026-07-25 16:30:01 -07:00
Satoshi Qazi Muhammed 3c7c4192b9 docs(backend): track backend deploy runbook (gitignored *.md override) 2026-07-25 11:02:16 -07:00
Satoshi Qazi Muhammed 80d273ff11 ci/cd(backend): offline test suite + Dockerize + audit-grade deploy (b + c)
Closes the "backend has zero CI + manual SSH deploy" audit finding for the indexer.

Part B — CI-runnable tests (the integration-tests/*.live.test.js need a live chain and
are excluded, not stubbed): extract injectable cores (indexer.js: DB+HTTP+decode, no
@pezkuwi import; council.js: createApp factory for the council/KYC routes). New node:test
suite (19/19, fully offline) — in-memory sqlite, dependency-injected chain stub, an
in-memory fake-supabase, and REAL @pezkuwi keyring signatures proving 401 bad-sig /
400 msg-mismatch / 200 valid / 409 dup / 403 non-member / threshold auto-execute. Backend
job now runs npm ci + npm test and is in ci-gate (failing tests block merge). Fixed a
latent bug: block indexing now awaits each insert (was fire-and-forget forEach). Added
runtime deps server.js imported but were missing from package.json (@supabase/supabase-js,
pino, pino-http) — server.js could not have `npm ci`-run before.

Part C — Dockerfile (non-root, HEALTHCHECK /health, sqlite state on a /data VOLUME kept
out of the image) + backend deploy pipeline mirroring the web one: build+push to GHCR,
cosign keyless sign + verify, ssh deploy with the DB volume preserved across deploys,
/health poll, auto-rollback to previous SHA, same Telegram CEO approval gate, main/tags
only, never fork PRs. New secrets documented in backend/DEPLOY.md (BACKEND_VPS_HOST/USER/
SSH_KEY). DB_PATH env (default ./transactions.db) lets prod point at the volume.

Note for owner: if server.js (council/KYC bootstrap) is legacy/unused, its newly-added
deps can be dropped instead.
2026-07-25 11:01:47 -07:00
Satoshi Qazi Muhammed 20a5602167 test(p2p): DB-backed proof of financial RLS + escrow REVOKE enforcement
Replicates the Supabase RLS model on a real Postgres engine (pglite) with roles + an
auth.role() JWT-claim stub and proves the harden_financial_rls + escrow-REVOKE
migrations enforce: anon cannot read balances / payment-method PII or execute
release_escrow_internal; the SECURITY DEFINER read path + service_role still work. 6/6.
Together with the freeze-trigger test and the 34 app-auth tests, the fund-custody
security layer is now proven-by-execution (logic); staging deploy is the in-situ step.
2026-07-25 10:20:38 -07:00
Satoshi Qazi Muhammed cd9bc577ae test(p2p): DB-backed proof of the trade fund-routing freeze trigger (BYPASS #1)
Runs the exact freeze trigger from migration 20260725040000 against a real Postgres
engine (pglite) and proves: anon UPDATE of buyer_id/crypto_amount (the escrow-redirect
attack) is BLOCKED; status/proof-only stays allowed; service_role is exempt; the
attacker redirect never persists. 5/5 pass. Upgrades the custody-critical trigger from
code-reviewed to proven-by-execution. RLS/REVOKE role-enforcement still needs a
multi-role staging Postgres (pglite is single-user).
2026-07-25 10:15:28 -07:00
Satoshi Qazi Muhammed a0492a41ac docs(p2p): track the security deploy runbook (ordered migrations, edge functions, secret names)
Consolidated gated-deploy runbook for the P2P security remediation. Contains no secret
values — only the names of the env vars/secrets the owner sets on staging/prod.
2026-07-25 02:46:40 -07:00