Marketplace Project: Secure the Marketplace
Learning Objectives
- You can integrate supplied authentication with the application API.
- You can enforce resource ownership at the authoritative operation.
- You can connect protected browser mutations to trusted session identity.
- You can test permitted and denied actions using real accounts and sessions.
In Part 4, we connected seller forms and buyer bids to the marketplace API. Those interactions still used temporary development identities. We will now connect them to real accounts and sessions, so the server resolves the current account and checks its permission for each operation.
Continue on master in marketplace/ after accepted milestone 26. You will integrate supplied authentication, connect
the browser’s account controls, and apply the resource rules from the teaching
chapters to the existing marketplace.
The note owner’s edit and the ticket’s assignment check give us a starting point. In this repository, the relevant relationships are seller ownership and buyer permissions:
| Teaching concept | Application here |
|---|---|
| Account and session views | Connect the supplied auth routes and use the marketplace_session cookie named in the session contract. |
| Note/document ownership and ticket relationships | Enforce seller ownership and buyer permissions with the current listing state. |
| Cookie, origin and CSRF checks | Connect the supplied server middleware and browser request helpers. |
| A permitted/denied comparison | Reuse the owner/outsider draft-edit pair and inspect the stored title and version. |
Each card names its work, checks and points. Use the shared project reference for repository setup and API contracts.
Server sessions and protected mutations
First, the API needs to identify the account independently of the fields a caller submits. We will connect the supplied session resolver and request guards before changing the browser workflow.
Receive milestone 27’s Git contribution and follow docs/authentication.md. It supplies credential/session services, cookie routes, origin/CSRF middleware, abuse protections, an additive migration, test helpers and lifecycle checks. Connect the routes and middleware to the existing Hono route chain, preserving inferred AppType for the client. Remove the server’s development-identity fallback.
Part 4 exports configured app and publicApi instances. This milestone introduces factory functions so tests can create an application with their own origin, cookie settings and clock. Follow the supplied integration steps, retaining the normal app export and the inferred route type.
Use the supplied authenticated-account helper to update existing HTTP test fixtures while preserving their assertions, including malformed-input and media-type rejections. Check permitted mutations and a raw rejected request before moving to the browser.
Marketplace milestone 27: integrate trusted identity
0 / 70 points
Start from accepted milestone 26. Use the shared project reference for setup, contracts, isolated checks, and submission.
Receive the authentication contribution
After milestone 26 passes, receive the supplied authentication modules, migration 005,
API test helper and lifecycle suite, docs/authentication.md, and
tools/prepare-authentication.mjs through Git:
git fetch origingit diff HEAD..origin/milestone-27git merge --ff-only origin/milestone-27Commit unfinished local edits before merging. If local commits prevent a fast-forward,
inspect the contribution and use git cherry-pick origin/milestone-27, preserving your
work. If a supplied path already exists, compare and resolve that file without replacing
your application. The contribution leaves your application routes, resource
implementations, configuration and lockfile intact.
Follow docs/authentication.md to preview and apply the dependency addition using the
existing client container, then refresh api/deno.lock with that runtime. The helper
preserves your other configuration values and can be rerun safely. Review git diff;
rebuild the API image after updating the lockfile.
Objective
Integrate the supplied authentication subsystem with the marketplace. It contains complete credential verification, persistent sessions, cookie handling, origin/CSRF middleware, and bounded abuse controls.
Keep these supplied implementations; this milestone assesses how application requests use them.
-
Apply the supplied migration 005 in isolated storage; do not rewrite migrations 001-004. Existing seed users have no password and cannot log in; create synthetic test accounts through the supplied registration route.
-
Turn Part 4’s singleton
publicApiandappsetup intocreateBrowserApiandcreateAppfactories using the steps indocs/authentication.md. Construct onecreateSecurityBoundary(cookieName)per app and connect its auth routes, actor resolver and supplied request middleware to the existing resource route chain. Preserve the handlers and inferredAppTypefor#api. Keep the ordinaryappexport and existing entry point.Configure the exact UI origin, credentialed CORS, and the documented cookie settings. Remove development-identity middleware and header fallbacks.
-
Use the resolved actor ID for draft creation, seller reads and bids. Call
security.withBidLimit(actorId, operation)around bid work and passsecurity.allowAuthAttemptto the supplied auth router; these controls are supplied and ready to use. Resource ownership checks follow in milestone 30. -
Replace the development identities in your existing draft-creation, draft-edit and bid-HTTP test fixtures with authenticated test accounts. The contribution supplies
api/tests/helpers/authenticated-account.ts; retain your tests’ assertions and cleanup. Input-rejection tests need valid session/origin/CSRF context so they reach the existing input rules; missing or non-JSON Content-Type still gives 400INVALID_INPUT. Run the supplied lifecycle suite to check that a forged development header cannot identify a caller and that rejected requests leave storage unchanged. It also covers missing/incorrect CSRF tokens.
Supplied boundary and application work
The supplied migration adds only account credential state and sessions. A safe user is
{id,displayName}. The
session contract
documents the existing login, registration, session and logout endpoints.
Keep the existing resource schemas, SQL repositories, transaction logic and Hono RPC
inference. Modify api/src/app.ts and api/src/routes/public-api.ts to connect the
boundary. Use the supplied protectSessionRequests middleware; do not reproduce cookie
parsing, session storage or password algorithms in resource handlers. Write
PERMISSION-MATRIX.md from the five-row shared matrix as the target for milestones
30-31.
Test and submit
Run the three updated inherited API test files, followed by the complete API suite
against the isolated database. The contribution includes
api/tests/supplied_session_lifecycle_test.ts; keep it unchanged. The ordinary suite
discovers it, or run it directly with
deno test --no-check --allow-env --allow-net tests/supplied_session_lifecycle_test.ts
using the shared API Compose command. It checks successful login/session resolution,
logout, expiry and request protections. Keep existing successful resource mutations
working with the correct session, origin and token.
Type checking remains useful for the inferred route contract and is optional for points. Submit the integrated source, supplied files, migration, lockfile and updated tests.
Points
- 25 points: registration, cookie sessions and current-user reads establish trusted identity; a development header cannot impersonate an account.
- 20 points: protected resource mutations enforce the supplied origin/CSRF and JSON-input checks, while valid requests still succeed.
- 15 points: expiry, disabled/deleted accounts and logout preserve the supplied session lifecycle contract.
- 10 points: the three updated inherited API tests pass and the permission matrix records the target policy.
These groups assess the existing integration separately.
Browser account controls and sessions
The API can now resolve a session. The browser needs to discover that account after a reload and update its view after login or logout. We will use the shared session view from Chapter 5.3 to connect these events.
Adapt that view to the accepted marketplace API. Resolve the session on startup and after login/logout; keep unknown, anonymous, and authenticated states distinct. Connect registration, login, and logout controls, and verify that the account controls reflect the confirmed session.
Receive milestone 28’s Git contribution and follow docs/account-workflow.md.
It supplies the session state, credential/CSRF request helpers, logout
coordination and client-fixture migration. Connect these helpers to the shared
transport and your typed account operations; you do not implement the token’s
storage, clearing or logout lifecycle. The browser continues to manage the
HttpOnly cookie.
Infer auth/session response types from milestone 27’s API route chain, while
preserving runtime status checks and useful rejection feedback. Keep the
transport’s existing credentials: "include" behavior for direct cross-origin
development requests; the supplied request helper applies that setting.
Preview and apply the supplied tools/migrate-session-test-fixtures.mjs using
milestone 28’s commands to update client fixtures without changing their
behavioral assertions. Implement the account workflow using these fixtures. The
marketplace screens and existing two-account browser journey are connected in
the next milestone.
Marketplace milestone 28: account controls and sessions
0 / 60 points
Start from accepted milestone 27. Use the shared project reference for repository setup, contracts, isolated checks, and submission.
Receive the account setup
After milestone 27 passes, receive the milestone 28 contribution in your accepted marketplace repository:
git fetch origingit diff HEAD..origin/milestone-28git merge --ff-only origin/milestone-28Commit unfinished local edits before merging. If local commits prevent a fast-forward,
inspect the contribution and use git cherry-pick origin/milestone-28, preserving your
work. If a supplied path already exists, compare and resolve that file without replacing
your application or tests. The contribution supplies:
client/src/lib/supplied/session-client.tstools/migrate-session-test-fixtures.mjsdocs/account-workflow.md
Objective
Connect the accepted server session API to the browser. The supplied module handles the CSRF plumbing; implement the typed account operations and interface.
| Supplied support | Your implementation |
|---|---|
| Shared account state and CSRF-token storage/clearing | Typed registration, login, session, and logout HTTP operations |
| Credentialed requests and CSRF headers on mutations | Expected-status checks, successful JSON, and structured errors |
| Session refresh after login/logout and logout token coordination | Account forms, pending/error feedback, and rendering the account state |
| Migration of inherited client fixtures | Connecting the helpers to your existing client |
The browser stores and sends the HttpOnly session cookie; JavaScript cannot read it. The
browser also creates the Origin header. The supplied helper includes credentials,
stores the separate CSRF token returned by /api/session, and sends that token on
mutations. Keep passwords out of persistent browser storage.
Follow docs/account-workflow.md to connect createSessionFetch(options) to both your
Hono client and existing fetch-based requests. Preserve status checks, read decoders,
and rejection feedback. Stop sending development identity headers. Infer successful
response types from #api.
Implement the typed operations in client/src/lib/api/auth-api.ts. For example:
const readSession = async () => { const response = await createTypedMarketplaceClient().api.session.$get(); if (response.status !== 200) return await accountFailure(response); return await response.json();};Use your structured API error handling in accountFailure, preserving HTTP status,
code, and message. Give it the signature
const accountFailure = async (response: Response): Promise<never> => { ... };:
Promise<never> tells TypeScript that the helper throws instead of returning a
successful value.
Registration accepts { email, displayName, password } and returns JSON on 201; login
accepts email, password and returns JSON on 200.
Logout accepts the supplied transport, calls
createTypedMarketplaceClient({ transport }).api.auth.logout.$post(), and expects 204
without decoding JSON. Bind these operations once:
import { createAccountClient } from "#lib/supplied/session-client.ts";
export const { getSession, register, login, logout } = createAccountClient({ session: readSession, register: registerAccount, login: loginAccount, logout: logoutAccount,});Render client/src/lib/components/Account.svelte at /account. Import the supplied
accountState readable store, call getSession() on startup, and render its unknown,
anonymous, and authenticated states. The authenticated state contains user.id and
user.displayName. Use the returned client methods for registration, login, and logout;
session refresh and token cleanup are supplied. Show pending feedback and errors.
Registration does not log in; tell the user to log in after success.
Use labels Email, Display name, and Password; buttons Register, Log in, and
Log out; Account created feedback; and Signed in as NAME for the current account.
Follow the shared
session responses and errors.
The next milestone connects the seller and bidding screens to this account state and updates the existing browser journey. Their development actor controls and existing caller arguments can remain until then; the server already derives identity exclusively from the session, and the shared transport no longer sends a development identity header.
Preserve the existing client fixtures
From marketplace/, preview and apply the routine fixture changes:
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --rm --no-deps \ --volume "$PWD:/workspace" --workdir /workspace client deno run --allow-read=client \ tools/migrate-session-test-fixtures.mjsdocker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --rm --no-deps \ --volume "$PWD:/workspace" --workdir /workspace client deno run --allow-read=client \ --allow-write=client tools/migrate-session-test-fixtures.mjs --writeReview git diff. The helper preserves the existing assertions while replacing
development identity with credentials and session fixtures. It also adapts
already-migrated fixtures to the supplied session module. It can be rerun safely and
stops without writing if it cannot recognize a fixture. Keep your tests and ask for an
adapted migration in that case.
Focused checks and points
Run the existing client check, test, and build tasks. deno task check includes the
typed auth client. Type checking is optional for points. Manually verify registration,
login, a session read after a page reload, and logout on /account. Keep the accepted
API tests passing. The inherited marketplace browser journey is adapted and assessed
next.
- 20 points: registration uses the typed client, sends the account fields, shows success feedback, and leaves the visitor signed out.
- 20 points: typed login, session reads and logout keep the displayed account and supplied session context consistent.
- 20 points: the shared transport connects the supplied credential and CSRF helpers, including read-only requests and cleared account state.
These groups award points independently after a successful client build.
Authenticated marketplace interactions
The account controls now follow the session; the seller and bidding screens need to use the same account. In milestone 29, connect these screens to the current account. Remove development actor selectors and obsolete identity fields from their schemas and API calls. The server already uses session identity; the browser must follow that same account.
Receive the Git contribution with the same-origin proxy, Compose override and
docs/authenticated-marketplace.md. Adapt the existing browser journey to
register and sign in a seller, publish a listing, then sign in a different buyer
and bid. Keep its existing assertions.
Marketplace milestone 29: authenticated marketplace interactions
0 / 60 points
Start from accepted milestone 28. Your account controls and shared transport now use server sessions. Use the shared project reference for contracts, checks, and submission.
Receive the browser setup
After milestone 28 passes, receive tools/browser-proxy.ts,
compose.browser-tests.yaml, and docs/authenticated-marketplace.md through Git:
git fetch origingit diff HEAD..origin/milestone-29git merge --ff-only origin/milestone-29Commit unfinished local edits before merging. If local commits prevent a fast-forward,
inspect the contribution and use git cherry-pick origin/milestone-29, preserving your
work. Compare and resolve any supplied path that already exists without replacing your
application or tests. These files provide the browser-test topology, not completed
screens.
Objective
Connect the existing seller and bidding screens to the shared account state. Resolve the session when entering a protected workflow, show sign-in guidance when anonymous, and use the confirmed account when rendering available actions. The API remains responsible for authorizing every request.
Remove browser development actor selectors, caller arguments and the obsolete actorId
API option. Keep client/src/lib/schemas/draft-schema.ts focused on editable draft
fields; account identity does not belong in its validated form data.
Use the shared credential and token transport from milestone 28 for marketplace requests. Preserve pending and rejection feedback, retained form input, and the existing explicit conflict-reload behavior.
The fixture migration from milestone 28 already supplies session reads for the existing client tests. Keep their assertions and use those fixtures with the connected screens.
Complete the existing browser journey
Use the supplied
browser test topology.
Merge the documented Vite allowed hosts into your existing configuration, preserving
other settings, and preserve an empty API base with ?? and full /api/... paths. The
Compose override supplies the exact browser-visible origin;
docs/authenticated-marketplace.md gives the run and shutdown commands.
Adapt e2e-tests/tests/student_mutations.spec.ts to register and sign in the seller,
create/publish the listing, then log out and register/sign in a different buyer before
bidding. Keep the existing mutation and rejection coverage.
Focused checks and points
Run client check, test, and build tasks. Type checking is optional for points. Run the authenticated inherited browser suite through the isolated proxy topology and verify seller draft creation/publication, logout, and the different buyer’s bid. Keep the accepted account controls and API checks passing.
- 40 points: the built marketplace uses the current account and completes the authenticated mutation workflow against the real API.
- 20 points: the same passing journey asserts the buyer’s accepted bid and detects an incorrectly rejected bid, retaining its existing rejection assertions.
The assertion group uses the existing journey and requires that journey to pass against the working application. The workflow behavior is assessed independently of the submitted test’s assertions.
Draft ownership and visibility
The browser no longer offers a development actor selector, but another caller can still send a request directly. We now need the draft operations themselves to enforce the seller relationship.
In milestone 30, apply the permission matrix to draft edits, publication, deletion and listing visibility. Keep ownership checks with the existing state and version checks in the same transaction. Test an owner’s successful edit and an outsider’s rejected edit, including the stored title and version.
Marketplace milestone 30: draft ownership and visibility
0 / 75 points
Start from accepted milestone 29. Use the shared project reference for contracts and isolated checks.
Continue after milestone 29
After milestone 29 passes, continue on master in your accepted marketplace/
repository. Before starting this milestone or submitting, run:
git fetch originFetching prepares milestone 30 for grading and makes its optional previous-step
reference at origin/milestone-30 available. It does not change your working files. You
do not need to merge the reference to complete this milestone.
Objective
Apply the draft rules from the permission matrix at the service boundary. Only a draft’s seller may edit, publish or delete it. Check ownership in the same locked transaction as resource state and version.
Public collection reads omit drafts; a signed-in seller can read their own draft through the detail route, while another caller receives 404. The caller ID always comes from the resolved session.
Keep the supplied request and abuse controls in place. Milestone 31 adds the separate bidding permission rule.
Files and test evidence
Modify api/src/services/listing-service.ts, api/src/routes/public-api.ts,
api/src/repositories/listing-repository.ts and
api/src/repositories/draft-repository.ts as needed. Include seller_id in the locked
draft row so the service can check its owner.
Add FORBIDDEN to ResourceError in api/src/services/marketplace-errors.ts and map
it to the documented 403 response.
Create api/tests/student_authorization_test.ts with one focused owner/outsider edit
comparison (an awaited t.step inside a Deno.test is sufficient).
Use the supplied authenticated-account helper for an owner and another account. First send a valid owner PATCH, assert 200 and inspect its stored title and incremented version.
Then send a different proposed title as the outsider, with that account’s valid session,
correct Origin, matching CSRF token, valid body and the current version. Assert 403
FORBIDDEN and immediately compare stored title/version with their values before this
request. These observations distinguish a permitted edit from an unchanged response, and
a rejected edit from a handler that writes before returning 403.
Keep test-owned accounts and resources, and clean them up explicitly. Reuse this file for the bidding comparison in milestone 31.
Use test-owned active listings in the inherited relationship-read regression because public browsing now excludes seeded drafts. Preserve its related seller/category assertions and explicit cleanup.
Test and submit
Run
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm api deno test --no-check --allow-env --allow-net tests/student_authorization_test.ts,
then the complete isolated API suite. Type checking is optional for points.
Points
- 55 points: ownership and listing visibility are enforced at the authoritative operation.
- 20 points: the owner/outsider comparison checks both the response and stored state, detecting a blocked owner, an allowed outsider or a write before rejection.
The implementation is checked independently. The comparison points require the submitted
integration suite to pass and detect the stated faults. Denied draft writes give 403
FORBIDDEN; missing or concealed detail reads give 404 NOT_FOUND.
Bidding permissions and final checks
Ownership has a different consequence for bidding: the seller may manage a draft, but may not bid on their own active auction. We will compare that denial with a different account’s permitted bid.
In milestone 31, reject self-bids and permit a different signed-in buyer’s valid
bid. Add the bidding comparison to the same student_authorization_test.ts file
used for draft permissions. Check that a rejected self-bid leaves the price,
version and bid rows unchanged.
Run that file with the supplied lifecycle and earlier policy/concurrency suites.
Keep the accepted seller-to-buyer journey in student_mutations.spec.ts unchanged
unless an application change requires an update.
Marketplace milestone 31: bidding permissions and secured integration
0 / 40 points
Start from accepted milestone 30. Use the shared project reference and keep milestone 29’s browser test topology.
Continue after milestone 30
After milestone 30 passes, continue on master in your accepted marketplace/
repository. Before starting this milestone or submitting, run:
git fetch originFetching prepares milestone 31 for grading and makes its optional previous-step
reference at origin/milestone-31 available. It does not change your working files. You
do not need to merge the reference to complete this milestone.
Add the bidding permission rule
Prevent a seller from bidding on their own listing. A different signed-in buyer can bid when the existing amount/deadline rules allow it.
Check the listing’s seller against the caller ID from the resolved session in the same locked transaction as the existing bid rules, before changing auction state.
Modify api/src/services/auction-service.ts and
api/src/repositories/auction-repository.ts as needed, and map the new FORBIDDEN
result through api/src/services/bid-rules.ts and api/src/routes/public-api.ts. A
rejected self-bid returns 403 FORBIDDEN without changing price, version or bid rows.
Keep the supplied request and abuse controls in place.
Extend the existing authorization check
Keep milestone 30’s owner/outsider edit comparison in
api/tests/student_authorization_test.ts. Add a second focused comparison to the same
file: assert that a self-bid returns 403 FORBIDDEN and inspect the unchanged price,
version and bid rows, then assert 201 for a different signed-in buyer’s valid bid and
check that it was stored.
Both comparisons may share one Deno.test, with or without separate awaited t.step
cases.
Use the supplied authenticated-account helper, test-owned accounts and resources, and explicit cleanup. Both requests need valid session/origin/CSRF context and input so they reach the bidding rule. Reuse the existing file and fixture approach.
Finish the integration
Milestone 27’s Git contribution already supplied
api/tests/supplied_session_lifecycle_test.ts. Keep that file unchanged in the
cumulative repository.
Keep the earlier stale-version, bid and concurrency suites. Reuse
e2e-tests/tests/student_mutations.spec.ts from milestone 29: its
seller-publication/buyer-bid journey exercises the connected cookie workflow.
Test and submit
Run the focused api/tests/student_authorization_test.ts, then the complete isolated
API suite, which discovers both authorization comparisons, the supplied lifecycle file
and earlier regressions. Run the accepted browser journey and client build using the
shared project commands.
Submit the cumulative repository with those existing files. Type checking remains useful and optional for points.
Points
- 20 points: self-bids are rejected without changing auction state, a permitted buyer can still bid, and the submitted comparison detects an incorrectly accepted self-bid.
- 10 points: the supplied lifecycle and retained stale-version, bid and concurrency API suites pass independently of the new comparison.
- 10 points: the accepted real-cookie two-account browser workflow still completes.
Keep the accepted repository for Part 6. Its next steps improve layout, keyboard interaction, and recovery feedback while preserving these security rules.
Shared project reference
This reference covers milestones 27-31. Implement each capability when its card is reached, preserving accepted behavior from earlier steps.
Receive supplied files from Git
Milestones 27, 28 and 29 add small contributions to the preceding accepted repository.
Follow each card’s git fetch, inspection and merge commands from a clean working
tree. Inspect the diff before merging; the contribution adds support files while
preserving your application and tests. If your local commits prevent a fast-forward,
follow the card’s cherry-pick instructions and preserve your work when resolving
conflicts. Run the supplied preparation helper only at the step named in the card.
Working repository and local checks
Paths such as services/... below are relative to api/src/; @src/ resolves to
that directory. The API uses Deno, Hono, Zod, sValidator, and postgres.js through
api/deno.json.
Docker Compose provides database, database-migrations, api, client, and
e2e-tests. compose.test.yaml removes exposed API/client ports and uses temporary
database storage. Use both files and a distinct project name for every test
command; the API receives DATABASE_URL from Compose before imports. From the
repository root, first apply migrations:
docker compose -p wsd-marketplace-test \ -f compose.yaml \ -f compose.test.yaml \ run --build --rm database-migrationsRun the focused commands in the current card. At the end, stop only this test project with
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml down.
Run suites sequentially. Inspect git diff, commit the requested source/tests
and any changed lockfile, and git push origin master to submit. Do not commit
secrets, dependencies, builds, or test results.
Run the full API suite with:
docker compose -p wsd-marketplace-test \ -f compose.yaml \ -f compose.test.yaml \ run --build --rm api deno task testFor a focused API test, replace the final task invocation with
deno test --no-check --allow-env --allow-net tests/FILE.ts.
Client checks use the same two Compose files and run --build --rm client, followed
by deno task check, deno task test, or deno task build.
From milestone 29 onward, use the three-file topology below for browser checks.
Type checking helps verify the inferred Hono contract but is not assessed in these five milestones. Each card assesses its stated behavior and submitted tests.
Permission matrix
| Actor and resource | Read | Change |
|---|---|---|
| Anonymous, active/closed listing | Public listing | None |
| Seller, own draft | Own/detail route | Edit, publish, delete |
| Other user, another seller’s draft | Concealed | None |
| Signed-in account other than this listing’s seller, active auction | Public listing | Bid under amount/deadline rules |
| Seller, own active auction | Public listing | No self-bid or draft edit |
Use the current server session for actor identity and the database for resource state. In milestone 30, enforce draft ownership alongside the existing state/version checks in the same transaction. Milestone 31 adds the bidding permission rule. This matrix requires no role or moderation feature.
Session contract and test identities
| Request | Body | Success response |
|---|---|---|
POST /api/auth/register | Strict {email,displayName,password} | 201 {user}; creates an account without logging in |
POST /api/auth/login | {email,password} | 200 {user} and a session cookie |
GET /api/session | None | 200 {user,csrfToken}, or {user:null,csrfToken:null}; Cache-Control: no-store |
POST /api/auth/logout | None | Empty 204 |
Logout requires the configured origin. With a current session it also requires that session’s CSRF token; if the session is already absent or expired, it clears the cookie and still returns 204. Protected resource mutations without a current session return 401 instead.
A public user contains only id and displayName. Never expose a password hash
or raw session token in JSON. The default cookie is marketplace_session with
Path=/, HttpOnly, SameSite=Lax and Max-Age=86400. COOKIE_SECURE enables
Secure for HTTPS; local HTTP tests explicitly use false.
Session tokens are 64 lowercase hexadecimal characters; only their SHA-256
hashes are stored. The separate CSRF token is stored with the session and exposed
by GET /api/session. Sessions expire 24 hours after creation, with no renewal
on use; disabled or deleted users cannot resolve a session.
The server requires the exact configured Origin for registration and login.
Authenticated mutations also require the session cookie and the matching
X-CSRF-Token from GET /api/session. The origin and token checks are combined
defenses: a request from an untrusted origin already fails the origin check.
| Responsibility | Component |
|---|---|
Send Set-Cookie after login and expire it after logout | The supplied Hono routes |
Store the cookie and add Cookie to eligible requests | The browser, subject to cookie policy and the request’s credentials setting |
Add Origin to these browser mutation requests | The browser |
Retrieve the CSRF token and add X-CSRF-Token to mutations | The client, using the supplied account/request helpers |
| Resolve the session and check request protection and permissions | The API |
Neither Svelte nor Hono RPC reads the HttpOnly cookie or constructs Cookie or
Origin. The shared transport configures Fetch’s credential handling; for
cross-origin login this also allows the browser to accept Set-Cookie.
Raw API tests have no browser cookie storage. The supplied
authenticatedAccount helper reads Set-Cookie and constructs the cookie,
origin and CSRF headers so permission tests reach the intended rule. This is
test-only simulation: browser JavaScript cannot read Set-Cookie or set its own
Cookie or Origin headers.
Errors use {error:{code,message}}. A protected resource route without a session
gives 401 UNAUTHENTICATED. Resource mutations reject either a wrong origin or
token with 403 CSRF_REJECTED; auth routes use 403 ORIGIN_REJECTED for a wrong
origin and logout uses 403 CSRF_REJECTED for a wrong token. Invalid body gives
400 INVALID_INPUT; failed login gives 401 INVALID_CREDENTIALS; duplicate
registration gives 409 ACCOUNT_UNAVAILABLE.
Create unique synthetic accounts per test, such as
test-<uuid>@example.test, with a 15-256 character password, and clean owned rows.
The application exports createApp({clientOrigin, readNow?, secureCookie?, cookieName?}) plus the configured app instance. Tests set the origin and optional
clock without starting a listener. Import the shared sql from @src/database.ts
for fixture queries. Group database checks as awaited t.step calls inside one
Deno.test, and close sql in finally after all steps.
Browser tests with the same-origin proxy
Use milestone 29’s tools/browser-proxy.ts and compose.browser-tests.yaml
throughout the later milestones. Merge
server.allowedHosts: ['client','e2e-proxy','localhost'] into your Vite config.
When reading the browser API base, use ?? for its fallback so that the override’s
empty string is preserved; || would replace it. An empty base keeps full
/api/... request paths on the browser’s current origin.
Figure 1 shows how the browser sees one origin while the proxy routes requests to
two services. The proxy forwards request cookies and Origin, and response
Set-Cookie headers. The API checks the browser-visible origin
http://e2e-proxy:8080, even though the proxy contacts it at http://api:8000.
The browser stores the host-only cookie for e2e-proxy, the host that returned
the response from its perspective, and sends it on eligible requests to that
host. The internal api hostname does not become the cookie’s browser-facing
host. The proxy forwards the headers; it does not create or validate the session.
Run and stop this isolated topology with:
docker compose -p wsd-marketplace-test \ -f compose.yaml \ -f compose.test.yaml \ -f compose.browser-tests.yaml \ run --build --rm e2e-testsdocker compose -p wsd-marketplace-test \ -f compose.yaml \ -f compose.test.yaml \ -f compose.browser-tests.yaml \ downExisting API contract
The baseline API uses full /api paths. POST /api/listings takes strict JSON
{title,description,categoryId,startingPrice} and returns 201 {listing}.
Title is trimmed to 1-120 characters, description has at most 4000 characters,
categoryId is an existing positive INTEGER, and price is integer cents
0-2147483647. A new draft starts at version 1 with null deadline/winner.
PATCH /api/listings/:id takes {version, ...editableFields}, requires at least
one editable field, and returns 200 {listing} with version incremented once.
POST /api/listings/:id/publish takes {version,endsAt}; endsAt is an ISO string
with an explicit timezone, strictly in the future. The supplied API schema
accepts seconds with optional fractional seconds and normalizes to a UTC ISO
string at millisecond precision. It returns 200 {listing} with active state
and a new version.
DELETE /api/listings/:id
allows drafts only and returns empty 204. GET /api/my/listings returns the
current session user’s bare listing array; GET /api/listings/:id returns a bare listing.
POST /api/listings/:id/bids takes only {amount} (integer cents 1-2147483647),
and returns 201 {bid:{id,listingId,amount,createdAt}, listing:{id,currentPrice,endsAt,status,version}}. The amount must exceed
currentPrice. After acquiring the listing’s database lock, the operation must
check that server time is still before endsAt. Accepted bids preserve the
deadline set when the listing was published.
Errors are {error:{code,message,fields?}}, with optional {path,message} field
entries. Missing resources give 404 NOT_FOUND; invalid bodies 400 INVALID_INPUT;
stale drafts 409 STALE_VERSION; non-drafts 409 NOT_DRAFT; invalid publication
deadlines 400 INVALID_DEADLINE; low bids 409 BID_TOO_LOW; closed/expired auctions
409 AUCTION_CLOSED. Rejections preserve stored state.
The client may use the documented mutation response types without adding runtime validators for those responses. Preserve the existing read decoders and their regression tests.
Listing representation
A listing JSON object contains numeric id, sellerId, categoryId,
startingPrice, currentPrice, and version; strings title, description,
sellerName, and categoryName; status (draft, active, or closed);
endsAt (ISO timestamp with milliseconds, or null); and winningBidId (integer,
or null). Names come from the related user/category records. Prices are integer
cents. Collection reads return a bare array ordered by listing ID ascending and
limited to 100. Path IDs match [1-9][0-9]* and are at most 2147483647.
Check Your Understanding
- Why must removing a browser actor selector also remove the server’s development-identity fallback?
- How do session validity and ownership answer different questions about a request?
- Why must a rejected self-bid leave the listing price and bid history unchanged?
- Why should a negative permission test begin with a valid session and valid request protections?
- What additional evidence does a real-cookie browser workflow provide beyond direct API tests?