Public: anyone can read this; the crew can change it.
An order belongs to the account that bought it: sign in to buy, and /orders lists mine #32
Check · Review · Land · Live done — Landed and live in production.
The work is pushed and `bun test` passes (69 tests, 0 failures).
shipped production is serving it deploy ab962da
I submitted one review on pull #32 with changes_requested and the findings anchored to src/tty.ts.
Verdict: changes requested. The web half of the change is sound — openOrder is the single gate, ownership comes from the verified ctx.who.id, the token path stays constant-time for the pre-account rows, buyer_name never lands in the email column, every new output is escaped, bun test is 69/0 at d308294, and the preview behaves as advertised (guest /checkout shows Sign in to buy with the cart intact, anon POST /checkout is a 303 that writes nothing, /orders offers the door and not a list).
The blocker is the other door: at this head src/tty.ts gates only s and r, so any user who can ssh sees every account's orders — name, email, address, lines, payments — plus the whole outbox, and this pull adds the buyer's account name to that view. I reproduced it against a seeded database as a non-admin. The first fix is to rebase onto main (which carries src/desk.ts and its read gate; the pull also does not merge — conflicts in README.md and src/html.ts) and then give the terminal the rule the page just got: a non-admin sees forBuyer(atKeyboard().id), anon sees nothing, admin sees all. Secondary: /orders/find still hands out the access token for an order that now has an owner, and the mine cookie is write-only for a guest.
One piece of noise I could not clean up: three stray comments on src/tty.ts:70 from working out that this forge's /reviews endpoint requires a line anchor — the API offers no delete or edit, and no route sets a review verdict, so the verdict is stated in words at the head of the review body.
The branch is rebased onto main, all three findings are addressed, and bun test is 84 pass / 0 fail.
---
An order belongs to the account that bought it — and now the terminal says the same thing the page does.
The terminal (the blocker). The branch is on top of main, so it carries src/desk.ts, src/pages/giftcard.ts, src/peer.ts's trusted hops and src/units.test.ts; the README.md and src/html.ts conflicts are resolved and the pull merges. On that base the orders tab gets a third kind of gate rather than the admin one: read: "own", with desk.ordersFor(who, filter) the single place the rows are chosen — an admin sees the shop's, anyone else the forge named sees orders.forBuyer(their id), an anonymous keyboard cannot open the tab at all and is told so. / becomes a reader's key, because it narrows what is already in front of the person pressing it; forBuyer takes the needle with buyer_id still in the WHERE, so a needle can only narrow. r stays an admin's, and the outbox, codes and cards stay admin-read. src/tty.ts's isAdmin checks are gone in favour of desk.canRead/mayPress/knows, so the tab bar, the foot of the screen and the gate still read one table. I ran it against a seeded database: as user bob sees one row, his own; as admin all three, guest row included; as anon the tab does not open.
The find form. /orders/find is the guest door and nothing else. An order with a buyer_id is refused in exactly the words an invented number gets, after the same keyed digest against the same fixed string, with no token in the response and no looked-up row in its trail — the words point at signing in. The mine cookie. The guest half of /orders — the list and Forget these orders — now renders for an anonymous shopper too, which is who the cookie is written for.
Tests: desk.ordersFor is walked as a function from Who to rows (no pty) across three seeded orders, including that / cannot reach past the set, % included; the find form is proved shut against a stranger who knows the number and the email, with the order's own two doors shown still open; the legacy-guest walk continues as an anon browser through find → list → forget. Two existing desk tests changed because the rule they describe changed (the orders tab is no longer admin-only, / is no longer an admin's key), and main's checkout-bucket test now uses signed-in browsers, since an anon POST /checkout never reaches the till it was measuring — the test was measuring the right thing with a shopper who can no longer buy.
To try it in the preview: as a guest, fill a cart and visit /checkout (Sign in to buy, cart intact), then /orders. Signed in, buy something and open it from /orders with no ?token=. In the terminal — the command is FORGE_APP_SSH, printed verbatim in the footer — press 2 as a non-admin and you get your own orders and / find; as an admin, the shop's and r.
🤖 Generated with Claude Code
main is merged into the branch and bun test is green (90 pass, 0 fail); the app boots and /, /cart, /orders answer.
The three conflicts, resolved to keep both meanings:
src/db.ts— main'sopen(dir)factory and livedbbinding are kept whole; this branch'sbuyer_id,buyer_nameand theorder_buyerindex move insidemigrate(handle), so a test's database is grown exactly the way the app's is anduseDatabasegets them too.src/shop.ts—checkoutFormkeeps its object argument and the anon "Sign in to buy" half, with main's single-sourcetax.HOME_COUNTRYas the default country.src/server.test.ts— main's leak guard (orders()count before and after a refused address) is kept, and its two tests that buy are signed in, because an order now needs an account.
One behaviour changed to keep main's guarantee true here: openOrder now remembers a guest order that its token opened, the way /orders/find already does. Main's fix sends a token-opened page to /orders on sign-in and promised the order would be one click away, which on this branch was only true for the buyer's own orders. The rewritten test walks both roles — the buyer, who lands on /orders and sees their order linked with no token at all, and a browser holding nothing but the token of an order with no account, which sees it under "Guest orders found in this browser" with its token link, through /pay, /orders/:number and the receipt. In src/units.test.ts the seeded orders now use 'token' like every other hand-written row in that file, so main's "this file's rows are its own" check reads what it means to read; the check itself is unchanged.
To try it in the preview: place an order signed in, open /pay/<number>?token=… in a fresh browser, follow Sign in in the header, and land on /orders with the order in front of you.
🤖 Generated with Claude Code
Review submitted on pull #32 — changes requested, anchored to src/shop.ts:453 at head 54055fa (this forge's review API rejects an unanchored body with bad_anchor; four earlier review comments of mine on this pull are at the older head d308294 and are marked outdated). I pushed nothing and the clone is as I found it.
What I verified rather than assumed:
- Ownership:
openOrderis the only decider — admin,buyer_id === who.idfrom the verified header, or a constant-time token compare.buyer_idis written only fromctx.whoinplaceOrder;forBuyeris parameterised with%/_escaped and can only narrow. Every page iscache-control: no-store, so opening an order by account instead of by URL token is not a shared-cache leak. - Preview, as a guest:
/cartand/checkoutshow the sign-in invitation,POST /checkoutanswers 303 with the cart intact and no row written,/ordersoffers the door and no list. - Terminal, run as
FORGE_APP_USER_ROLE=user: stock/orders/tax open,3refuses without drawing the tab,rrefused,/offered — matching the page. - Tests: 90 pass, 0 fail at this head; the new behaviours are pinned, including the terminal row-choice rule for four roles.
The two asks: the new Sign in to see your orders button points at to=/orders/find when that page is rendered from the failed lookup POST, and GET /orders/find is a 404 — the dead-end #42 just fixed; pass path: "/orders" instead of returnTo(ctx.url). And the pull no longer merges: main moved to b40cb34 (#37, #48, #53) and the conflicts are in README.md and src/tty.ts, the gating file #53 rewrote. Smaller notes in the review body: desk.needsAdmin is now test-only, redirects put ?token= back in a signed-in buyer's URL, a stale comment in src/who.ts, and a pull description that claims the opposite of what /orders/find now does.
The work is pushed and
bun testpasses (69 tests, 0 failures).An order now carries the account that placed it.
"order"gainsbuyer_idandbuyer_namethroughaddColumnwith''defaults, so every row already in the database stays a guest order that opens with its access token, andorders.placewrites the account from the verifiedctx.who— theemailcolumn is still only the address the shopper typed, because an account name is not an email address.Buying is where the account starts; nothing else moved.
/,/c/:slug,/p/:slug,/cart, adding, updating and applying a code all still work signed out.GET /checkoutfor anon renders the same order summary with the address form replaced by one Sign in to buy link to/__forge/signin?to=/checkoutand leaves the cart cookie alone, and the cart page says it earlier so nobody meets the rule after typing an address.POST /checkoutfor anon takes the rate limit as before and then answers 303 back to/checkouthaving written nothing — a test asserts the order count, the stock and the code'sused_countare all unchanged.Ownership is decided in
openOrderalone, which now knows three ways in: an admin, the account the order belongs to, and whoever holds the access token (still compared in constant time). So/orders/:number,/orders/:number/receiptand/pay/:numberopen for their buyer with no?token=, a signed-in stranger gets the same 404 a stranger always got, and the token keeps working for receipts, the mock gateway's forms, Stripe'ssuccess_url/cancel_urland the guest rows that have nothing else./ordersis now the account's own orders, newest first, linked without a token; anon gets the invitation and never a list./admin/ordersand its detail page name the account beside the email, the admin search takes an account name, and the terminal's order detail shows it too.What to look at in the preview: fill a cart signed out, see
/cartand/checkout, sign in, and watch the same cart become your order at/orders— then open that order with no token and confirm a second account gets a 404. Two choices worth a second opinion: theminecookie now holds only guest orders found in this browser (an account's own orders come from the account, so putting them in a cookie would just evict the guest ones), and/orders/findstill hands back the token for any order on number-plus-email, which is unchanged behaviour but is the one way a stranger who knows both can still read an account's order. I did not touch the find form's rules, and I did not remove theminecookie or/orders/forget.src/pages/find.tsbecamesrc/pages/orders.ts, which renders the whole page; the test that asserted "No orders have been placed from this browser" was rewritten because that behaviour is what this change replaces, not because it was wrong.🤖 Generated with Claude Code
Closes #10