Authentication, Authorization & Security

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 conceptApplication here
Account and session viewsConnect the supplied auth routes and use the marketplace_session cookie named in the session contract.
Note/document ownership and ticket relationshipsEnforce seller ownership and buyer permissions with the current listing state.
Cookie, origin and CSRF checksConnect the supplied server middleware and browser request helpers.
A permitted/denied comparisonReuse 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:

Terminal window
git fetch origin
git diff HEAD..origin/milestone-27
git merge --ff-only origin/milestone-27

Commit 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.

  1. 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.

  2. Turn Part 4’s singleton publicApi and app setup into createBrowserApi and createApp factories using the steps in docs/authentication.md. Construct one createSecurityBoundary(cookieName) per app and connect its auth routes, actor resolver and supplied request middleware to the existing resource route chain. Preserve the handlers and inferred AppType for #api. Keep the ordinary app export and existing entry point.

    Configure the exact UI origin, credentialed CORS, and the documented cookie settings. Remove development-identity middleware and header fallbacks.

  3. Use the resolved actor ID for draft creation, seller reads and bids. Call security.withBidLimit(actorId, operation) around bid work and pass security.allowAuthAttempt to the supplied auth router; these controls are supplied and ready to use. Resource ownership checks follow in milestone 30.

  4. 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 400 INVALID_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:

Terminal window
git fetch origin
git diff HEAD..origin/milestone-28
git merge --ff-only origin/milestone-28

Commit 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.ts
  • tools/migrate-session-test-fixtures.mjs
  • docs/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 supportYour implementation
Shared account state and CSRF-token storage/clearingTyped registration, login, session, and logout HTTP operations
Credentialed requests and CSRF headers on mutationsExpected-status checks, successful JSON, and structured errors
Session refresh after login/logout and logout token coordinationAccount forms, pending/error feedback, and rendering the account state
Migration of inherited client fixturesConnecting 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:

Terminal window
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.mjs
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 \
--allow-write=client tools/migrate-session-test-fixtures.mjs --write

Review 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:

Terminal window
git fetch origin
git diff HEAD..origin/milestone-29
git merge --ff-only origin/milestone-29

Commit 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:

Terminal window
git fetch origin

Fetching 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:

Terminal window
git fetch origin

Fetching 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:

Terminal window
docker compose -p wsd-marketplace-test \
-f compose.yaml \
-f compose.test.yaml \
run --build --rm database-migrations

Run 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:

Terminal window
docker compose -p wsd-marketplace-test \
-f compose.yaml \
-f compose.test.yaml \
run --build --rm api deno task test

For 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 resourceReadChange
Anonymous, active/closed listingPublic listingNone
Seller, own draftOwn/detail routeEdit, publish, delete
Other user, another seller’s draftConcealedNone
Signed-in account other than this listing’s seller, active auctionPublic listingBid under amount/deadline rules
Seller, own active auctionPublic listingNo 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

RequestBodySuccess response
POST /api/auth/registerStrict {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/sessionNone200 {user,csrfToken}, or {user:null,csrfToken:null}; Cache-Control: no-store
POST /api/auth/logoutNoneEmpty 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.

ResponsibilityComponent
Send Set-Cookie after login and expire it after logoutThe supplied Hono routes
Store the cookie and add Cookie to eligible requestsThe browser, subject to cookie policy and the request’s credentials setting
Add Origin to these browser mutation requestsThe browser
Retrieve the CSRF token and add X-CSRF-Token to mutationsThe client, using the supplied account/request helpers
Resolve the session and check request protection and permissionsThe 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.

Figure 1. The browser uses one origin; routing to separate services leaves security decisions with the API.

Run and stop this isolated topology with:

Terminal window
docker compose -p wsd-marketplace-test \
-f compose.yaml \
-f compose.test.yaml \
-f compose.browser-tests.yaml \
run --build --rm e2e-tests
docker compose -p wsd-marketplace-test \
-f compose.yaml \
-f compose.test.yaml \
-f compose.browser-tests.yaml \
down

Existing 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

  1. Why must removing a browser actor selector also remove the server’s development-identity fallback?
  2. How do session validity and ownership answer different questions about a request?
  3. Why must a rejected self-bid leave the listing price and bid history unchanged?
  4. Why should a negative permission test begin with a valid session and valid request protections?
  5. What additional evidence does a real-cookie browser workflow provide beyond direct API tests?