# zlattea-new - Full Codebase Audit Date: 2026-07-16 Scope: `/home/mleku/s/zlattea.com/zlattea-new` (working tree, `zlattea-new.zip` deployment artifact, `data/` SQLite DBs, `dist/` build output) Method: full source read, `bun test` execution, SQLite schema inspection, diff of zip artifact vs working tree, runtime probes of module resolution and CLI commands. --- ## 1. What this is Not actually an SPA. It is an **Astro static MPA** (`output: 'static'`) with vanilla JS inline scripts per page, plus a separate **Hono API server** run under Bun on port 3001. Stack: - Astro 7.0.9 (static build -> `dist/`) - Hono 4.x API (`src/server.ts` -> `src/api.ts`) - `bun:sqlite` (`data/shop.db`, WAL) - bcryptjs for passwords, DB-token sessions - Stripe / LNURL-pay / BTCPay code present (see section 5 - currently unreachable) - Cart state in `localStorage`, no framework Layout: ``` src/pages/ index, shop, product/[slug], cart, checkout, about, delivery, order/[id] src/layouts/ Base.astro (header/footer/login modal + inline i18n dict + cart badge) src/lib/ products.ts, products.json, seed.ts, db.ts, auth.ts, orders.ts, payments.ts, couriers.ts, i18n.ts src/api.ts Hono routes src/server.ts entry src/setup.ts installer/seeder tests/e2e.test.ts, src/test.test.ts install.sh / start.sh / dev.sh / teardown.sh / uninstall.sh / zlattea-api.service zlattea-new.zip self-hosted deployment artifact (downloaded by install.sh) ``` --- ## 2. Headline finding: three divergent generations of the same app coexist The working tree, the zip artifact, and the live SQLite DB are three different generations that do not agree with each other: | Artifact | State | |---|---| | `zlattea-new.zip` (packed 11:18) | Older sources. Its `api.ts` has a full checkout: writes orders matching the real DB schema, calls Stripe/LNURL/BTCPay, returns `{orderId, status, payment}`. Contains `Caddyfile` and `zlattea-web.service`, which do not exist in the working tree. | | Working tree (edited 14:00-16:35, after the zip) | Regressed. `api.ts` rewritten: payments removed entirely, checkout delegates to a new `orders.ts` whose column list matches no schema anywhere. `auth.ts` session insert rewritten against a non-existent schema. `setup.ts` imports a function that no longer exists. | | `data/shop.db` | Schema from the `setup.ts` generation, data from an even older seed (16 products incl. duplicate English rows `bilberry`/`chamomile` named "Билка"/"Лайка"). | Product data exists in three places and disagrees: DB (16 rows, used by nothing), `src/lib/products.json` (14 items, slug `blueberry`), inline array in `src/lib/products.ts` (14 items, slug `borovinka`). Frontend and API use only the inline array; the DB products table is vestigial; `products.json` is dead but still shipped and seeded from. Consequence: production, if installed from the zip, runs different code than the tree. Rebuilding the zip from the tree would ship the regressed version and also break `install.sh` (it runs `cp Caddyfile /etc/caddy/` under `set -euo pipefail`; the Caddyfile exists only inside the old zip). --- ## 3. Verified broken functionality (test-backed) `bun test`: **6 fail / 17 pass** across 23 tests. Each failure below was reproduced on this tree. ### 3.1 `bun run setup` crashes `src/setup.ts:4` imports `seedProducts` from `./lib/seed`; `seed.ts` exports only `seedDatabase`. Verified by the e2e run: `SyntaxError: Export named 'seedProducts' not found`. First-time setup is dead in the working tree. ### 3.2 All logins fail (email and Nostr) `src/lib/auth.ts:32` does `INSERT INTO sessions (id, user_id)` but the sessions table is `(token, user_id, expires_at NOT NULL)`: wrong column name plus a missing NOT NULL value. Every `createSession` throws; the API catches it and returns 400. Verified: `api > handles login` and `api > handles nostr auth` both fail with 400. Additionally `getUserFromSession` (auth.ts:38-40) queries `sessions WHERE id = ?` and selects `users.is_admin`; the `is_admin` column is not created by any schema in the repo. Session validation would throw even if inserts worked. Session expiry logic (present in the zip generation) was deleted. ### 3.3 Checkout 500s `src/lib/orders.ts:26` inserts columns `payment_status, shipping_method, courier_office, customer_name, customer_email, customer_phone, notes` - none exist in the orders table (`setup.ts:82` / live DB: `email NOT NULL, payment_id, shipping_address, courier`). It also omits `email`, which is NOT NULL. Verified: `api > handles checkout` fails with 500. Even if the schema matched: the client (`checkout.astro:179-192`) sends `{items, email, shipping:{...}, courier, paymentMethod}` while `createOrder` reads `data.customer_name`, `data.customer_email`, `data.shipping_address`, `data.payment_method` - every field would be silently null. Totals are computed from `item.price_cents`, which the cart doesn't contain (3.4), so `total_cents` would be NaN. ### 3.4 Cart arithmetic is broken in the shipped `dist/` `shop.astro:82` and `product/[slug].astro:80` store cart items as `{slug, name, price, image, qty}`. `cart.astro:45` and `checkout.astro:106` read `item.price_cents`. Result: NaN line totals and `NaN лв` grand total. Confirmed in the current build artifact: `dist/shop/index.html` contains `push({slug:n,name:r,price:i,...})` while `dist/cart/index.html` and `dist/checkout/index.html` reference only `price_cents`. ### 3.5 Order confirmation page cannot exist `src/pages/order/[id].astro:3` has `getStaticPaths() { return []; }` under `output: 'static'`, so zero order pages are generated. Verified: `dist/order/` does not exist. Every completed checkout redirects to `/order/` -> 404. ### 3.6 Client/server checkout contract mismatch API returns `{id, status, total_cents}` (`api.ts:74`); the client reads `data.orderId` and `data.payment` (`checkout.astro:197-209`). `orderId` is always undefined -> redirect to `/order/undefined` (also 404 per 3.5). `data.payment` never exists because the payment layer was removed from the API - the Stripe/Lightning/onchain radio options in the UI are decorative. ### 3.7 `astro start` is not a command `package.json` `"start": "astro start & bun run src/server.ts"` and `start.sh:17` invoke `astro start`, which prints CLI help and exits (verified). Production "start" never serves the site. The systemd unit runs only the API; static serving depends entirely on the Caddyfile that exists only inside the old zip. ### 3.8 No `/api` proxy in any local mode Pages fetch relative `'/api/...'` URLs. Only the zip's Caddyfile maps `/api/*` to `localhost:3001` (via `handle_path`, which correctly strips the prefix since the Hono routes are unprefixed). `astro dev` (port 3000, `bun run dev`) and `astro preview` (`dev.sh`) have no proxy configuration, so every API call from the browser 404s locally. Login, city autocomplete, offices, and checkout are non-functional in every mode the provided scripts can start on a dev machine. ### 3.9 Test suite disagrees with the code - `src/test.test.ts` expects `data.orderId` and `data.payment.type` from checkout - the removed zip-generation API contract. - Product tests expect English fixture names ("Mashterka") seeded into a mocked DB, but `products.ts` is a hardcoded array; the `mock.module('./lib/db')` is a no-op for products. Two failures stem from this. - `tests/e2e.test.ts` imports `existsSync`/`join` twice (lines 2-4 and 270-271) and aborts in `beforeAll` because of 3.1, skipping all its assertions. ### 3.10 Type-level defects - `src/lib/products.ts:6` declares `id: number`; every element supplies `id: "1"` strings. The interface requires `description_en`, missing from all 14 records. Unused imports (`readFileSync`, `fileURLToPath`, `dirname`, `join`) remain at the top. - `src/lib/seed.ts:6` calls `getDb(dbPath)`; `db.ts:5` `getDb()` takes no parameters - the argument is silently ignored, DB selection happens via `DB_PATH` env only. - `seed.ts` creates a products table (`INTEGER PRIMARY KEY AUTOINCREMENT`, `name_en`, `description_en`) that conflicts with the one `setup.ts:80` creates first (`id TEXT PRIMARY KEY`, no `name_en`). If setup ran, seeding would then fail on the missing columns. --- ## 4. Security findings ### 4.1 Nostr login is not authentication (critical) `POST /auth/nostr` (`api.ts:42-55`) accepts any 64-hex string, calls `findOrCreateNostrUser`, and issues a session. No challenge, no signature verification. Anyone can log in as any Nostr user by POSTing that user's public key. Currently masked only by bug 3.2 - fixing sessions without fixing this enables account takeover. ### 4.2 `verifyNostrChallenge` is dead code and can never succeed `auth.ts:43-54` uses `require('@noble/curves/secp256k1')`; @noble/curves 2.2.0 exports only `./secp256k1.js` subpaths. Verified at runtime: `Cannot find module '@noble/curves/secp256k1'`. The catch swallows the error and returns false. Nothing calls this function in the working tree. Even the zip generation treated challenge/signature as optional (bypass by omission), and no server-side challenge issuance/storage exists, so a correct verifier would still be replayable. ### 4.3 Client-controlled prices (critical once checkout works) `orders.ts:24` (and the zip API alike) computes totals from `item.price_cents * item.qty` taken from the request body. No server-side price lookup against the catalog, no quantity bounds, no stock check. Price tampering is trivial. ### 4.4 Unauthenticated order retrieval `GET /orders/:id` returns full order rows - name, email, phone, address (PII) - with no auth. IDs are UUIDv4 (hard to enumerate) but confirmation URLs leak into history/logs/referrers. ### 4.5 Default admin credentials `install.sh:72` and `setup.ts:45` default `ADMIN_PASSWORD` to `pa55word`. A production install without env overrides creates `admin@zlattea.com` with that password. Currently no admin functionality exists, but the account and login path do. ### 4.6 Wide-open CORS, secrets in tree, no rate limiting `api.use('/*', cors())` without options on auth routes. `.env` and `.env.bak` (with generated JWT_SECRETs), `data/shop.db` + WAL/SHM, `data/test.db` all sit in the tree. The directory is not a git repository; `.gitignore` provides no actual protection. No rate limiting on any endpoint; `bcrypt.compareSync` on the login path is a cheap CPU-exhaustion target. ### 4.7 Deployment pipeline issues - `install.sh` is a root-implied `curl | unzip | run` from `https://zlattea.smesh.lol` with no checksum/signature. - `uninstall.sh` is `curl -sL ... | bash` (remote code execution by convention). - `teardown.sh:19-23` deletes `/etc/caddy/Caddyfile` wholesale if grep finds `zlattea` - destroys co-hosted sites' config. - `install.sh` copies the entire build directory (including `.env`, `node_modules`, DB) to `/opt/zlattea`. --- ## 5. Dead code and unused dependencies | Item | Status | |---|---| | `src/lib/payments.ts` | Entirely unreachable - nothing imports it in the working tree. All Stripe/LNURL/BTCPay capability is dead. Contains hardcoded fake exchange rate (1 BGN cent = 10 sats). LNURL invoice fetch does not validate description hashes or amount bounds. BTCPay Greenfield API call reads `data.addresses.BTC` - that field does not exist in the Greenfield Invoice API response. | | `src/lib/i18n.ts` | Never imported. `Base.astro` carries its own duplicate inline translations (lines 194-351). | | `jsonwebtoken` dep | Not used. Sessions are random 32-byte hex DB tokens, not JWTs. | | `lnurl` dep | Not used. LNURL-pay is handled via raw fetch in payments.ts (which is itself dead). | | `nanostores` dep | Not used. Cart state is vanilla localStorage + `window.dispatchEvent(CustomEvent)`. | | `dotenv` dep | Superfluous - Bun loads `.env` automatically. CLAUDE.md explicitly says not to use dotenv. | | `bcryptjs` `crypto.randomUUID()` `crypto.randomBytes()` | Fine. bcrypt cost factor 10 is reasonable. | | `index.ts` | Boilerplate: `console.log("Hello via Bun!");` | | `src/test.test.ts:199` | Tests `GET /api/auth/session` - route is `/auth/session` on the API, not `/api/auth/session`. Only works because Hono's test request is relative. The test name/URL convention is just misleading. | | `node_modules/` | 282MB, contains full handlebars (?) from Astro's transitive deps. | --- ## 6. Data integrity issues ### 6.1 Three product datasets | Source | Count | Slug for blueberry | Notes | |---|---|---|---| | `data/shop.db` products table | 16 | `borovinka` (id=9), `bilberry` (id=13) | Duplicate "Билка" and "Лайка" rows. Created by old seed, never used by frontend/API. | | `src/lib/products.json` | 14 | `blueberry` | Only read by `seed.ts` during fresh setup. | | Inline array in `src/lib/products.ts` | 14 | `borovinka` | What frontend and API actually serve. | `setup.ts` creates the products table (TEXT id). `seed.ts` creates it again (INTEGER AUTOINCREMENT id, plus `name_en`/`description_en` columns absent from setup's schema). Neither schema matches the live DB's 16 rows. ### 6.2 Frontend image mismatches Product 6 (`Жълт кантарион`, slug `zhult-kantarion`) uses image `/images/products/StJohn.jpg` - that file is `StJohn.jpg`. Capitalization-sensitive on case-sensitive filesystems (Linux is fine; macOS APFS default is not). Products 2, 4, 7 use generic "background" images (`home-2background-img-6.jpg`, `parallax-1.jpg`, `smladlika-bilkata-chudo-koiato-lekuva-253.jpg`) rather than the actual herb photos. Some image filenames contain Cyrillic characters (`ЧЕРВЕНО-ПОДЪБИЧЕ.jpg`, `мента-билка-765x510-1.jpg`). Caddy serves these correctly. Several unused images exist in `public/images/products/` (e.g., `laika.jpg`, `kopriva.jpg`, `menta.jpg` were the original singles - the inline products.ts already points to other filenames, making these orphaned assets). ### 6.3 Courier office data is entirely placeholder `src/lib/couriers.ts` lists ~120 offices across three couriers. None of the addresses correspond to real courier offices - they are `ул. Цар Симеон 1` pattern repeated across every city. "Банско" is incorrectly placed under Sofia's city list (id prefix `speedy-blg-2`, `econt-blg-2`, `bgposts-blg-2`). No courier API integration exists. --- ## 7. i18n defects - The `bg` object in `src/lib/i18n.ts` contains duplicate keys: `aboutText3`, `deliveryTitle`, `deliverySpeedy` and others are defined twice, and the second set (lines 81-96) is English text placed inside the Bulgarian dictionary. JS object semantics mean the last key wins, so Bulgarian users would see English for those keys. Moot only because the file is dead code. - The live translation dict is a second, drifted copy embedded in `Base.astro` (lines 194-351). Example drift: `loginNostr` maps to "or" in Base.astro but "Login with Nostr" in i18n.ts. - `checkout.astro` uses namespaced keys (`checkout.title`, `payment.stripe`, `btn.place-order`) that exist in neither dictionary, so the language switcher cannot translate that page at all. - The entire checkout page is written in transliterated Latin Bulgarian ("Plashtane", "Danni za dostavka", "Porychai", "Nalozhen platezh") while every other page uses Cyrillic. This includes user-facing alert() strings. - Language preference is applied client-side after load (flash of Bulgarian for EN users); `` is static. --- ## 8. Functional gaps against the README claims | Claim | Reality | |---|---| | "Shopping cart (localStorage)" | Works mechanically but displays NaN totals (3.4). | | "Checkout with Stripe + Lightning (Bitcoin)" | Bug 3.3 + 5: checkout returns 500 or neutered responses. No Stripe.js client library loaded, no Payment Element, no Confirmation flow. The checkout alert() claiming success because a `clientSecret` exists is false - nothing was charged. | | "Nostr login (NIP-07 browser signers)" | Bug 4.1: accepts any pubkey without proof. NIP-07 getPublicKey is called client-side but its result is sent unverified to a server that issues a session unconditionally. | | "Courier shipping (Speedy, Econt, BG Posts)" | Bug 6.3: offices are fake. No courier API calls for label generation, tracking, or real-time availability. No shipping cost calculation (the free-over-50-BGN claim in delivery.astro has no matching server logic). | | "Fully responsive" | CSS has `@media (max-width: 768px)` breakpoints. Not independently tested but existing rules appear functional. | | "Bulgarian language" | Bug 7: checkout is Latinized, some translations are English due to duplicate keys. | Additionally missing (not claimed, but expected of an e-commerce shop): - No stock management (in_stock is a static 1, never decremented). - No order status lifecycle (orders are created pending and never transition). - No payment webhooks (Stripe/LNURL-pay/BTCPay listen endpoints do not exist). - No email confirmations or receipts. - No admin UI or dashboard. - No SEO metadata, sitemap, robots.txt, or Open Graph tags. - No 404 page (Caddy fallback references `/{err.status_code}.html` but no `404.html` is generated). - No favicon (the browser hits a 404 for `/favicon.ico`). --- ## 9. Minor issues - `setup.ts` prompt() is broken in two ways: it derives env var names from the question string (fragile), then checks `process.stdin.isRaw` (nearly always falsy). `Bun.stdin.readSync()` is not a Bun API function. In practice prompts always return the default; non-interactive mode always fires. - Unused imports: `products.ts` imports `readFileSync`, `fileURLToPath`, `dirname`, `join` that are never called. `jsonwebtoken` is listed as a dependency but `import jwt from 'jsonwebtoken'` appears nowhere. - `test.test.ts:70-72` `mock.module('./lib/db'` is called after `beforeAll` has already run and after the first import of `products.ts` - the mock's `getDb` is a no-op for the inline product array. - `test.test.ts:37-42` `db.query('INSERT INTO orders...')` references columns `payment_status` and `customer_name` that don't exist in the test's own CREATE TABLE statement (line 34-48). - `src/lib/db.ts` `closeDb()` is exported but never called except in `seed.ts`. - `seed.ts` has a `closeDb()` call at line 27 inside the early-return branch that also runs at line 54; the DB is opened before the early return, so the close is duplicated for that path, but both work. - HTML: No `main` landmark on shop/cart/checkout pages. No `aria-label` on interactive elements besides cart and language toggle. Missing ``. - `node_modules` is included at the project root rather than inside `zlattea-new/` for the zip; the install script runs `bun install` anyway, making the packaged modules dead weight. - Cyrillic characters in image filenames (`ЧЕРВЕНО-ПОДЪБИЧЕ.jpg`, `мента-билка-765x510-1.jpg`). Caddy serves them fine in UTF-8. Future git archive/zip extraction on non-UTF-8 locales can corrupt them. - `zlattea-new.zip` includes `.astro/` cache directory and `bun.lock`, which are rebuild artifacts. --- ## 10. Summary - what actually functions The following paths work on the currently deployed (zip) build when behind Caddy: 1. Static pages (index, shop, product pages, about, delivery, cart) - render correctly from the dist. 2. Category filtering on shop via query params - client-side only, full page reload. 3. Product detail pages via `getStaticPaths()` - all 14 slugs generated. 4. Add-to-cart mechanics correct (will produce correct cart JSON; price display broken per 3.4). 5. Language toggle works for keys present in Base.astro's inline dict. 6. Caddy reverse proxy and static serving (zip's Caddyfile is correct). Everything else - authentication, checkout, payments, order history, courier selection, order confirmation - is broken in the working tree, partially working in the zip (checkout writes orders but payments return dev mocks), and non-functional in dev mode. The 282MB `node_modules` directory for this project contains 27 deps (2 direct, 25 transitive through Astro). Astro itself brings significant weight (handlebars, svgo, sharp, shiki, vite, esbuild, rollup, etc.). For a static 7-page site with 14 products, the infrastructure-to-content ratio is approximately 1000:1 by filesystem weight. --- ## 11. Priority-ordered remediation list Ordered so each item unblocks the next; foundation before symptoms. 1. **Consolidate to one generation.** Pick the tree as canonical. Delete `products.json` + DB products table OR make the DB the single product source; one schema, one seed path. Fix `setup.ts` import (`seedDatabase`), align `getDb()` signature. Regenerate the zip only from a state that passes the test suite. 2. **One schema, migrated.** Write the definitive DDL: `sessions(token, user_id, expires_at)`, `orders(...)` matching what `orders.ts` inserts, add `users.is_admin`. Fix `auth.ts:32` (`token`/`expires_at`) and restore expiry checking. Fix `orders.ts` insert list and map the actual client payload (`email`, `shipping.*`, `paymentMethod`) to columns. 3. **Fix cart field naming.** `price` -> `price_cents` in `shop.astro:82` and `product/[slug].astro:80` (or read `price` in cart/checkout - one name, both sides). 4. **Restore the checkout contract.** Reinstate the payment layer from the zip's `api.ts` (`orderId`, `payment` in the response) but with server-side price lookup from the catalog - never trust `item.price_cents` from the client. 5. **Fix order confirmation.** Either make `/order/[id]` a purely client-side page that fetches `/api/orders/:id` (keep static shell, remove the empty `getStaticPaths` trap by using a query param route `/order?id=...`), or switch Astro to hybrid output for that route. 6. **Real Nostr auth.** Server issues a stored, single-use challenge; client signs a kind-22242-style event via NIP-07 `signEvent`; server verifies with `@noble/curves/secp256k1.js` (note the `.js`) against the event id. Session only on valid signature. 7. **Serve correctly.** Remove `astro start` from package.json and `start.sh`. Production = Caddy (static dist + `/api` proxy) + systemd API unit. Dev = add a proxy (Astro `server.proxy` via vite config) so `/api/*` reaches :3001 locally. 8. **Bring Caddyfile and zlattea-web.service into the tree** so the zip can be rebuilt without silently losing deployment files. 9. **Secrets hygiene.** Delete `.env.bak`, remove `data/*.db*` from the tree, init git so `.gitignore` means something, change the default admin password behavior to refuse setup without an explicit password. 10. **Then** re-run `bun test` and rewrite the assertions that encode the old contract; add a test for the cart price field naming (would have caught 3.4). 11. Lower priority: real courier office data (Speedy/Econt both have public office-list APIs), shipping cost calculation, payment webhooks and order state transitions, email confirmation, single i18n source with proper keys, Cyrillic checkout copy, admin UI. --- ## Appendix A - test run evidence ``` bun test v1.3.14 17 pass / 6 fail, 73 expect() calls, 23 tests, 2 files fail: products > finds product by slug (fixture-name mismatch) fail: api > serves product by slug (fixture-name mismatch) fail: api > handles login (400 - sessions INSERT throws) fail: api > handles nostr auth (400 - sessions INSERT throws) fail: api > handles checkout (500 - orders schema mismatch) fail: tests/e2e.test.ts beforeAll (setup.ts seedProducts import error) ``` ## Appendix B - live DB state ``` data/shop.db: products: 16 rows (ids 1-16, includes duplicate bilberry/chamomile English rows) orders: 0 rows sessions schema: (token TEXT PK, user_id TEXT NOT NULL, expires_at INTEGER NOT NULL) users schema: no is_admin column ``` ## Appendix C - zip vs tree divergence (files that differ) ``` src/api.ts tree removed payments, renamed routes, changed response shapes src/lib/auth.ts tree broke session insert/lookup, dropped expiry src/lib/seed.ts tree renamed seedProducts -> seedDatabase, changed schema src/lib/products.ts tree inlined the catalog (zip read products.json) src/lib/orders.ts tree-only (source of checkout 500) src/lib/products.json blueberry vs borovinka slug src/layouts/Base.astro, i18n.ts, index/shop/about/delivery/product pages: drift zip-only: Caddyfile, zlattea-web.service ```