From d80f8ebcf079912d71653d29e02020e9b4f5d303 Mon Sep 17 00:00:00 2001 From: Mathieu Audebert Date: Tue, 13 Oct 2026 15:27:02 +0200 Subject: [PATCH 1/3] test(worker): a visitor with no account leaves a note, on a worker with accounts (FRU-224) Mathieu, 2026-10-13: an account is always possible for whoever administers, and is not needed to leave a note. The code did it already, and no test held the whole path on a worker whose sites come from the console: the ones that did ran on FRUITBACK_CLIENTS, which is going. - `visitor.test.ts`: a site made through the console, a note with no session, no token and no cookie, read again, refused from another origin; a site for members answers 401 to a read. - `e2e/visitor.spec.ts`: the same in a browser, where CORS stops a page of another origin. - Measured, and written in SECURITY.md: a site for members still takes a note that has no session. - The token signed by the site is not decided, and nothing here changes it. Co-Authored-By: Claude Opus 5.5 --- SECURITY.md | 22 ++++ apps/worker/src/visitor.test.ts | 205 ++++++++++++++++++++++++++++++++ docs/decisions/worker.md | 22 ++++ docs/modes.md | 16 +++ e2e/visitor.spec.ts | 116 ++++++++++++++++++ 5 files changed, 381 insertions(+) create mode 100644 apps/worker/src/visitor.test.ts create mode 100644 e2e/visitor.spec.ts diff --git a/SECURITY.md b/SECURITY.md index 475735ad..5b8d7dd1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -538,6 +538,28 @@ client**: an empty map is not the absence of a map, which would answer every pag file holds addresses, names and the id each provider gives a person, no password, and no token of a provider that signs a person in. Since FRU-121 it also holds the key of each connector, encrypted: see below. `FRUITBACK_CLIENTS` is refused beside it, so one map has one source. +**A visitor with no account leaves a note on a site that everyone reads, and that is the design** +(FRU-224, a decision of 2026-10-13): no session, no token, no cookie. The page sends the id of the +site and the browser sends its `Origin`; the worker refuses an origin that is no site of the worker +(`origin-not-allowed`) and the origin of another site (`origin-not-allowed-for-client`), and keeps +the name the visitor typed as a claim, never as verified. `visitor.test.ts` holds that path on a +worker whose site was made in the console, with the notes in PostgreSQL: +test:`leaves a note and reads it again, with no session, no token and no cookie`, +test:`refuses a note and a read that name the site from the page of another site`, +test:`keeps the name the visitor typed, and never says the worker verified it`. In a browser, +`e2e/visitor.spec.ts` adds what CORS does: test:`a page of another origin reads no note of the site, +and plants none`. As above, `Origin` binds a claim to a site and authenticates nobody: a request +that is not from a browser sends the one it wants, or none. + +**The visibility of a site decides who reads it, and not who writes on it.** On a site read by +`members`, a read with no session answers `401` +(test:`reads no note: the worker asks for a session, and says so`). **A note sent with no session is +still kept there**, as a claim, as on every site: its sender cannot read it back, and the members +find it, in the worker or in the tracker of the site +(test:`still takes a note sent with no session, which its sender cannot read back`). This is what +the worker did before FRU-224, measured there, and not a choice made there. A team that wants +notes from its members only has no setting for it today. + **With `FRUITBACK_DATABASE_URL` the same accounts are in a PostgreSQL database** (FRU-141), in place of that file, and the worker refuses to start with both. The database holds what the file holds: addresses, names, the id each provider gives a person, diff --git a/apps/worker/src/visitor.test.ts b/apps/worker/src/visitor.test.ts new file mode 100644 index 00000000..04202b00 --- /dev/null +++ b/apps/worker/src/visitor.test.ts @@ -0,0 +1,205 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { type Seed, canonicalizePageUrl } from '@fruitback/shared'; +import { seedFixture } from '@fruitback/shared/seed.fixture'; +import { type World, callOn, signInTo, workspaceIn, worlds } from './console-world.fixture.ts'; +import { rowsOf } from './postgres.fixture.ts'; +import { createPostgresSeedStore } from './seeds-postgres.ts'; + +/** + * A visitor leaves a note with no account (FRU-224), on a worker that has accounts. + * + * Mathieu, 2026-10-13: « on peut créer un compte quoiqu'il arrive aussi pour la partie admin, par + * contre pas forcément besoin de compte pour laisser des notes côté user ». The code was there: a + * site that everyone reads takes a note from a page with no session and no token. No test held the + * whole path on a worker whose sites come from the console: the ones that did ran on + * `FRUITBACK_CLIENTS`, which is going (FRU-184). + * + * Each case goes through `handleRequest`, on PostgreSQL (PGlite), with better-auth on. An admin + * signs in and makes the site through the route of the console. The visitor then sends what the + * `