Imported from h4nzs/nyx-chat (
AGENTS.md). Install upstream withnpx skills add h4nzs/nyx-chat. Copyright stays with the author.
AGENTS.md
NYX: zero-knowledge post-quantum messenger. pnpm monorepo: web (React 19 + Vite 8), server (Express 5 + Prisma 7 + Redis bridge), marketing (Astro), packages/shared (@nyx/shared), server/transport-sidecar (Rust WebTransport).
Commands that matter
@nyx/sharedis consumed fromdist/— after editingpackages/shared/src, runpnpm --filter @nyx/shared run buildBEFORE typechecking web/server or the changes are invisible.- Build:
pnpm -r run build(root) — runs webtsc -b && vite build, servertsc, marketingastro build. - Unit tests:
pnpm -r --if-present run test. Server =node:testviatsx(files listed explicitly in serverpackage.json; jest config is dead, do not use). Web = vitest. - Typecheck:
npx tsc --noEmitper package. Afterpnpm installor dep updates, runnpx prisma generateinserver/first or server tsc fails with missingPrismaClientexport. - ESLint is broken repo-wide (typescript-eslint rejects TS 7.0). CI lint job is
continue-on-erroron purpose. Verify withtsc --noEmit+ build instead; don't try to fix lint errors. - E2E:
pnpm exec playwright test --project=chromiuminweb/. Prereqs: Postgres + Redis running, server dev (pnpm --filter nyx-server dev), vite dev (pnpm --filter nyx-web dev).workers: 1(serial — suite is flaky in parallel).e2e/global.setup.tswipes the local DB viaserver/scripts/reset-test-env.ts. - Playwright config webServer uses pnpm filter names
nyx-server/nyx-web(NOTserver/web).
WebTransport / transport.spec
- Rust sidecar:
cargo build --releaseinserver/transport-sidecar, run withJWT_SECRET,REDIS_URL,TRANSPORT_PORT=33333. It prints a cert hash at startup → paste intoweb/.envasVITE_TRANSPORT_CERT_HASH(dev pinning). transport.spec.ts/chat.spec.tsauto-skip whenWebTransportis undefined (headless Chromium on this machine lacks it). Use Chrome full build (--project=chrome) in CI (e2e-chromejob builds + runs the sidecar and parses the cert hash).- Sidecar ignores opcode
0x00on uni-streams/datagrams (CHAFF); auth also uses0x00but over the bidi control stream. Rust hardcodes opcode numbers — sync withpackages/shared/src/transport.ts.
E2EE / crypto constraints (do not break)
- Do not split
web/src/workers/crypto.worker.ts,crypto-worker-proxy.ts, or other crypto files (explicit maintainer rule); fix them in place only. - Frozen formats: at-rest prefix
ENC1:(keychainDb), XChaCha envelopebase64url(nonce(24)||ct)(canonical worker opsxchacha_seal/xchacha_open),ENCRYPT_DATAJSON must keepArray.from(...)number arrays (JSON.stringify of Uint8Array silently breaks stored bundles), 8KB traffic-cover padding. Never change primitives/padding/protocol. - Group chain keys / skipped keys / story keys are encrypted at-rest with masterSeed; migration runs on unlock via
saveDeviceAutoUnlockKey. - Zod 4 must run jitless (prod CSP has no
unsafe-eval). Set it ONLY via direct mutationglobalThis.__zod_globalConfig.jitless = true— usingzod.config()gets tree-shaken in prod builds because zod declaressideEffects: false. Done inpackages/shared/src/schemas.tsandweb/src/zodSetup.ts(first import in main.tsx). Don't remove, and don't addunsafe-evalback to nginx CSP. - Message pipeline lives in
web/src/lib/messagePipeline.ts(extracted from the store). Own-message decrypt failures returnwaiting_for_key, never error bubbles.
Server gotchas
- ESM TS with
.jsimport specifiers (run withtsx).utils/holds shared helpers (sessionUtils.ts,validate.tsincl.safeEqualStrings). - Prisma 7:
prisma db push(no migrate flow) readsDIRECT_URL || DATABASE_URLviaprisma.config.ts; pass--urlexplicitly if env isn't loaded. Prisma 7 refuses db push withoutPRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTIONenv (AI guard) — set it in scripts/CI, ask a human first. server/src/lib/prisma.tsappendssslmode=requireONLY for non-local hosts — local Postgres must stay plaintext (self-signed snakeoil cert otherwise kills every query withTlsConnectionError). Do not regress this.- Rate limiting uses atomic Lua INCR+EXPIRE (
redisBridge.tsRATE_LIMIT_LUA, auth pow, sandbox newchat) — do not reintroduce incr-then-expire races. - Single-active-device check runs per-opcode in
redisBridge.ts(isActiveDeviceAllowed, 60s cache). CSRF server state is keyed per client viax-nyx-installation-idheader (app.ts) — clientweb/src/lib/api.tsmust send that header consistently (same value asgetPersistentInstallationId()), or login/register break with 403. - Redis pub/sub:
nyx:upstream:<opCode>/nyx:downstream. Relay payloads must NOT duplicatecontentinto aciphertextfield (mappers.ts).
Frontend gotchas
- Single REST client:
web/src/lib/api.ts(api,authFetch,apiUpload).api-client.tswas deleted — don't recreate. - i18n: locales in
web/public/locales/{en,es,id,pt-BR}(7 namespaces,load: 'languageOnly'). Every key must exist in ALL 4 languages or console spam showsmissingKey; add new keys to all four. - App.tsx: global modals are
React.lazy+ render-on-demand; VirtuosoitemContentreads messages via a ref (keepmessagesout of its deps — that caused mass re-renders). - Login/new-device flow:
hasRestoredKeysgates routing (ProtectedRoute). Never add unconditionalnavigate("/chat")after login — routing must followhasRestoredKeysso the recovery modal isn't bypassed. nukeProtocol.executeLocalWipemust call/api/auth/logout-allfirst (HttpOnly cookies can't be cleared client-side).
Deploy / ops
- Push to
maintriggersdeploy.yml: CI builds everything + Rust sidecar, zips, SCP to VPS, pm2 restartsnyx-api/nyx-sidecar, and overwrites/root/nyx-app/server/.envwith/root/nyx-app/.env— prod secrets live only on the VPS (.envfiles are gitignored;.env.exampleat root andserver/are the templates). - Prod DB is LOCAL Postgres on the VPS (
postgres://nyx:…@127.0.0.1:5432/nyx_app); old Aiven host is dead. DB password is in/root/.nyx_db_pass(0600); daily backup cron dumps to/root/backups/. - CI:
ci.yml(build, unit, lint-nonblocking,pnpm audit --prod, e2e with Postgres/Redis services,e2e-chromefor WebTransport specs). Install always uses--frozen-lockfile; pnpm pinned viapackageManager: pnpm@11.4.0. - VPS is small (1 core, ~1GB RAM, 2GB swap,
vm.swappiness=10, Postgres tuned for low memory) — keep memory usage in mind when changing workers/queues.
Testing notes
- E2E registration walks a fixed modal chain (Proof of Trust → Recovery → Secure Phrase → Verify Sequence ×2 close → System Init). Helpers are duplicated per spec file — that's the repo convention; keep them in sync.
- Registration/avatar assertions can be slow — use
expect.poll/ generous timeouts, not shorter ones. - Unit tests must not require Postgres/Redis (server tests use fakes for Prisma clients).