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.
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:
dist/)src/server.ts -> src/api.ts)bun:sqlite (data/shop.db, WAL)localStorage, no frameworkLayout:
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)
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).
bun test: 6 fail / 17 pass across 23 tests. Each failure below was reproduced on this tree.
bun run setup crashessrc/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.
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.
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.
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.
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.
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.
astro start is not a commandpackage.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.
/api proxy in any local modePages 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.
src/test.test.ts expects data.orderId and data.payment.type from checkout - the removed zip-generation API contract.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.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.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.
verifyNostrChallenge is dead code and can never succeedauth.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.
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.
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.
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.
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.
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.| 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. |
| 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.
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).
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.
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.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.<html lang="bg"> is static.| 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):
/{err.status_code}.html but no 404.html is generated)./favicon.ico).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.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.main landmark on shop/cart/checkout pages. No aria-label on interactive elements besides cart and language toggle. Missing <meta name="description">.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.ЧЕРВЕНО-ПОДЪБИЧЕ.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.The following paths work on the currently deployed (zip) build when behind Caddy:
getStaticPaths() - all 14 slugs generated.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.
Ordered so each item unblocks the next; foundation before symptoms.
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.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.price -> price_cents in shop.astro:82 and product/[slug].astro:80 (or read price in cart/checkout - one name, both sides).api.ts (orderId, payment in the response) but with server-side price lookup from the catalog - never trust item.price_cents from the client./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.signEvent; server verifies with @noble/curves/secp256k1.js (note the .js) against the event id. Session only on valid signature.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..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.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).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)
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
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