AUDIT.md raw

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:

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:

ArtifactState
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.dbSchema 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/<id> -> 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

3.10 Type-level defects

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

5. Dead code and unused dependencies

ItemStatus
src/lib/payments.tsEntirely 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.tsNever imported. Base.astro carries its own duplicate inline translations (lines 194-351).
jsonwebtoken depNot used. Sessions are random 32-byte hex DB tokens, not JWTs.
lnurl depNot used. LNURL-pay is handled via raw fetch in payments.ts (which is itself dead).
nanostores depNot used. Cart state is vanilla localStorage + window.dispatchEvent(CustomEvent).
dotenv depSuperfluous - 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.tsBoilerplate: console.log("Hello via Bun!");
src/test.test.ts:199Tests 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

SourceCountSlug for blueberryNotes
data/shop.db products table16borovinka (id=9), bilberry (id=13)Duplicate "Билка" and "Лайка" rows. Created by old seed, never used by frontend/API.
src/lib/products.json14blueberryOnly read by seed.ts during fresh setup.
Inline array in src/lib/products.ts14borovinkaWhat 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

8. Functional gaps against the README claims

ClaimReality
"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):

9. Minor issues

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