diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed227d..3e26f95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,17 +27,6 @@ jobs: - run: npm ci - # TODO: BEGIN REMOVEME (okTurtles/chel#150) - # chel cannot fetch its own SQLite library, so scripts/chel.ts points it - # at the system one. Usually already present. - - name: Make sure the system SQLite library is there - run: | - if [ ! -f /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 ]; then - sudo apt-get update - sudo apt-get install -y libsqlite3-0 - fi - # TODO: END REMOVEME (okTurtles/chel#150) - - run: npx playwright install --with-deps chromium - run: npm run lint diff --git a/AGENTS.md b/AGENTS.md index 8bec713..516e356 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ src/chelonia/offline.ts the queue for writes made while the server is away src/types.ts the shapes the app passes around src/components/ Vue, and nothing else scripts/build-contracts.ts chel manifest -> chel pin -> manifest CID -scripts/chel.ts runs chel from node_modules, see below +scripts/chel.ts runs chel from node_modules docs/data.md the todos slot and the writes, with code docs/login.md signup and login, step by step docs/sharing.md how sharing works, message by message @@ -59,9 +59,9 @@ signal the process group or use the port: `lsof -ti:8000 | xargs kill`. After a full rebuild, restart `npm run serve`. Vite empties `dist/` and a server that was already running answers 404 until it is restarted. -The app is built with `LIGHTWEIGHT_CLIENT=true` (see `vite.config.ts`), the -same as Group Income: the browser keeps no message log, and Chelonia reads each -contract's HEAD from the saved state. +Chelonia runs as a lightweight client, which is `@chelonia/lib`'s default since +2.0.0, the same as Group Income: the browser keeps no message log, and Chelonia +reads each contract's HEAD from the saved state. The contract version comes from `version` in `package.json`. Editing a contract without bumping it makes the build stop, since the app would then be built @@ -72,45 +72,8 @@ against a manifest the accounts already on the server do not have. Each one is fenced in the source with `TODO: BEGIN REMOVEME (issue)` and `TODO: END REMOVEME (issue)`, so `grep REMOVEME` finds them all. -Several of these are already fixed upstream but not published. The app pins -`@chelonia/lib` 1.5.0 and `@chelonia/cli` 3.4.0, so a merged fix changes nothing -here until there is a release to bump to. - -- `scripts/chel.ts` and `.github/workflows/ci.yml`: the published - `@chelonia/cli` 3.4.0 cannot load SQLite on its own, so chel is run with - `DENO_SQLITE_PATH` pointing at the system library. Fixed by - [chel#162](https://github.com/okTurtles/chel/pull/162), tracked as - [chel#150](https://github.com/okTurtles/chel/issues/150). Until a release - ships, run chel as `node scripts/chel.ts `. -- `src/contracts/identity.js`, `src/chelonia/config.ts`, - `scripts/build-contracts.ts`: the account contract has to be named - `gi.contracts/identity`, Group Income's name, because chel only accepts a - contract created without an account to bill it to under that exact name. - [chel#160](https://github.com/okTurtles/chel/issues/160). -- `src/chelonia/auth.ts`, `lookupUsername`: replaced by - `chelonia/out/nameToContractID`. Merged as - [libcheloniajs#95](https://github.com/okTurtles/libcheloniajs/pull/95), - tracked as - [libcheloniajs#90](https://github.com/okTurtles/libcheloniajs/issues/90). -- `src/chelonia/auth.ts`, signup error message: the publish error carries the - HTTP status, so signup can say why it failed. Merged as - [libcheloniajs#97](https://github.com/okTurtles/libcheloniajs/pull/97), - tracked as - [libcheloniajs#94](https://github.com/okTurtles/libcheloniajs/issues/94). - `src/chelonia/auth.ts`, `USERNAME_REGEX`: a copy of chel's private `NAME_REGEX`. Goes once chel exports the rule. -- `src/chelonia/offline.ts`, `ensureRandomUUID`: `@chelonia/lib` builds - persistent action ids with `crypto.randomUUID`, which browsers only provide - on https and localhost, so the demo breaks over the LAN. Merged as - [libcheloniajs#101](https://github.com/okTurtles/libcheloniajs/pull/101), - tracked as - [libcheloniajs#100](https://github.com/okTurtles/libcheloniajs/issues/100). - `src/chelonia/auth.ts`, the key list in `signup`: gets shorter once [libcheloniajs#91](https://github.com/okTurtles/libcheloniajs/issues/91) lands. Not a removal, so it is a plain TODO. - -## Accounts from before lists existed - -Their todos were a slot on the identity contract and this version does not look -there. The account, the username and the password still work; the old todos do -not appear. There is no migration, since nothing has shipped. diff --git a/package-lock.json b/package-lock.json index eeaae0e..ab21500 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,13 @@ "license": "MIT", "dependencies": { "@chelonia/crypto": "1.0.1", - "@chelonia/lib": "1.5.0", + "@chelonia/lib": "2.0.0", "@sbp/sbp": "2.4.1", "buffer": "6.0.3", "vue": "3.5.13" }, "devDependencies": { - "@chelonia/cli": "3.4.0", + "@chelonia/cli": "3.4.1", "@eslint/js": "9.17.0", "@playwright/test": "1.49.1", "@types/node": "22.10.5", @@ -29,7 +29,7 @@ "vue-tsc": "2.2.0" }, "engines": { - "node": ">=22" + "node": ">=22.18" } }, "node_modules/@apeleghq/multipart-parser": { @@ -95,9 +95,9 @@ } }, "node_modules/@chelonia/cli": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@chelonia/cli/-/cli-3.4.0.tgz", - "integrity": "sha512-bBV/teQDhawaxt6Gc4IQH//XtB1gHsviR3HSLyY4zJ87T06hnxOoLVY8DeT7PL8n0TTPNEdZafSgc9hZGclvgA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@chelonia/cli/-/cli-3.4.1.tgz", + "integrity": "sha512-6hoAD1tNJ8E5cHjkoeBnEub906943pjizS+WOCG6s+3WFvLR2tbxR+O4knvmDZb4sEkgGLRnz1jbli3tguAj9g==", "cpu": [ "arm64", "x64" @@ -113,17 +113,17 @@ "chel": "bin/chel.js" }, "optionalDependencies": { - "@chelonia/cli-arm64-darwin": "3.4.0", - "@chelonia/cli-arm64-linux": "3.4.0", - "@chelonia/cli-x64-darwin": "3.4.0", - "@chelonia/cli-x64-linux": "3.4.0", - "@chelonia/cli-x64-win32": "3.4.0" + "@chelonia/cli-arm64-darwin": "3.4.1", + "@chelonia/cli-arm64-linux": "3.4.1", + "@chelonia/cli-x64-darwin": "3.4.1", + "@chelonia/cli-x64-linux": "3.4.1", + "@chelonia/cli-x64-win32": "3.4.1" } }, "node_modules/@chelonia/cli-arm64-darwin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@chelonia/cli-arm64-darwin/-/cli-arm64-darwin-3.4.0.tgz", - "integrity": "sha512-PNsrhMNq+RAm70zxVwQq7Lhs7CGwPWLEc0vx9X3/kgcq02Khk7tBiKApWR2qqp9yS0GVQ4/FQi1DGQLE4mJw6w==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@chelonia/cli-arm64-darwin/-/cli-arm64-darwin-3.4.1.tgz", + "integrity": "sha512-PVmhleAvcCyE+9WGLXUg3GBYH9NnDNGADxa7NZx8JSGhUwI6x8ufBvxEaBO9aNpMOotK/hdnabG3aYH60FczcQ==", "cpu": [ "arm64" ], @@ -132,15 +132,12 @@ "optional": true, "os": [ "darwin" - ], - "bin": { - "chel": "chel" - } + ] }, "node_modules/@chelonia/cli-arm64-linux": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@chelonia/cli-arm64-linux/-/cli-arm64-linux-3.4.0.tgz", - "integrity": "sha512-WbHG1nETHFBEVwsRRY2uGREPTB8D6MOa6ltva/5pKx6Ruuy11BRWAxtV6ADBruuq49dpGcYlR+zjv4g2q2RxUw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@chelonia/cli-arm64-linux/-/cli-arm64-linux-3.4.1.tgz", + "integrity": "sha512-EGjrVX7nANPsPOCiZxg7wvdJLrMUy+gHSFJUVkVNfNa7KbL0e5eIN05IVDfIT4P4fgBDS4t/p8X5TxtN63g4bg==", "cpu": [ "arm64" ], @@ -149,32 +146,12 @@ "optional": true, "os": [ "linux" - ], - "bin": { - "chel": "chel" - } - }, - "node_modules/@chelonia/cli-x64-darwin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@chelonia/cli-x64-darwin/-/cli-x64-darwin-3.4.0.tgz", - "integrity": "sha512-VmoH7O2MjEF17pnTuozE58nSca4o1xYa9gS8JrWuXVX0VbRUSoDad4nnU2zPDCdofrwsciyHU1DalF6y9c6Ykw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "AGPL-3.0", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "chel": "chel" - } + ] }, "node_modules/@chelonia/cli-x64-linux": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@chelonia/cli-x64-linux/-/cli-x64-linux-3.4.0.tgz", - "integrity": "sha512-ey7BqNiIOY2PkpN/UYxjpmOG1A7HmplyadP80omXTG1DVu1fbz7ro/d4gRPtHaxPRkNJTS24lDejc5/rT/3RMA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@chelonia/cli-x64-linux/-/cli-x64-linux-3.4.1.tgz", + "integrity": "sha512-GPpUjAHZ5xqW1d11BVnbbs0G0XFYYF70Rpq0UT4yEuSgCJUeI7Rm/ANG2nOfLup/YXJh+MYH5ihGBD8DwHY1PQ==", "cpu": [ "x64" ], @@ -183,15 +160,12 @@ "optional": true, "os": [ "linux" - ], - "bin": { - "chel": "chel" - } + ] }, "node_modules/@chelonia/cli-x64-win32": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@chelonia/cli-x64-win32/-/cli-x64-win32-3.4.0.tgz", - "integrity": "sha512-qmxVWrNG3kYUzLMdsjALU2hBq4lHwPpBoD8ccXLwv7//7i79JJDmqB2T5C/BKabIU4Rcp0sP9B6Z2XZONiW00Q==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@chelonia/cli-x64-win32/-/cli-x64-win32-3.4.1.tgz", + "integrity": "sha512-KdAO17mMTBVbKeAm62qWohNHnOsYgU+iBsm5Xj4Irjm69ynoCTQv6RMNaAM+s2cin/RH9S/xWvJC7xwtmrq2Sg==", "cpu": [ "x64" ], @@ -200,10 +174,7 @@ "optional": true, "os": [ "win32" - ], - "bin": { - "chel": "chel.exe" - } + ] }, "node_modules/@chelonia/crypto": { "version": "1.0.1", @@ -217,9 +188,9 @@ } }, "node_modules/@chelonia/lib": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@chelonia/lib/-/lib-1.5.0.tgz", - "integrity": "sha512-nJ1cgt5v7OF2XRvdKdATiUxWDVvCiBRgaT2NNo7ESPs2S2OROnj+oMt96NrmvAuX7BU6feu/yBxrLBtN6Bk6GQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@chelonia/lib/-/lib-2.0.0.tgz", + "integrity": "sha512-JRtJ0IwpRltXQrswa1o0VEX15LRY52BF82bT/DQSuEAR5VAViNy08SNm8mlS0NMxcKafozlUZmJXTQxDZj+EBw==", "license": "AGPL-3.0", "dependencies": { "@apeleghq/multipart-parser": "1.0.18", @@ -230,7 +201,7 @@ "@sbp/okturtles.eventqueue": "1.2.1", "@sbp/okturtles.events": "1.0.1", "scrypt-async": "2.0.1", - "turtledash": "1.0.3", + "turtledash": "2.0.0", "tweetnacl": "1.0.3" }, "peerDependencies": { @@ -3245,9 +3216,9 @@ } }, "node_modules/turtledash": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/turtledash/-/turtledash-1.0.3.tgz", - "integrity": "sha512-Z7LIPDSvnl0U53DyJ7YxLxG/yQac/NK0OwGNOM37SjE87S5KtybFEwHVhevUWtK8iaEzYHWyC5/vEd1GKm5iQw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/turtledash/-/turtledash-2.0.0.tgz", + "integrity": "sha512-EqkzVI49LZju/y8KyRb4L6/FyZRS/ZPV28vsWk+L4J+j5zUtnMpmUOfPw2AKy17UbzjcwjgKG3xHyJntT6k/8w==", "license": "MIT" }, "node_modules/tweetnacl": { diff --git a/package.json b/package.json index 778f37b..e5f420a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chelonia-todomvc", - "version": "0.2.0", + "version": "0.3.0", "private": true, "type": "module", "engines": { @@ -22,13 +22,13 @@ }, "dependencies": { "@chelonia/crypto": "1.0.1", - "@chelonia/lib": "1.5.0", + "@chelonia/lib": "2.0.0", "@sbp/sbp": "2.4.1", "buffer": "6.0.3", "vue": "3.5.13" }, "devDependencies": { - "@chelonia/cli": "3.4.0", + "@chelonia/cli": "3.4.1", "@eslint/js": "9.17.0", "@playwright/test": "1.49.1", "@types/node": "22.10.5", diff --git a/scripts/build-contracts.ts b/scripts/build-contracts.ts index 7fc6644..3ed4e62 100644 --- a/scripts/build-contracts.ts +++ b/scripts/build-contracts.ts @@ -16,12 +16,7 @@ import process from 'node:process' import { chel } from './chel.ts' const CONTRACTS = [ - // TODO: BEGIN REMOVEME (okTurtles/chel#160) - // Until the fix is released, chel only accepts a contract created without an - // account to bill it to when the manifest name is exactly this. Rename then. - // TODO: END REMOVEME (okTurtles/chel#160) - { name: 'gi.contracts/identity', file: 'identity.js' }, - // A list is created by an identity, so it is attributed and its name is free. + { name: 'todomvc/identity', file: 'identity.js' }, { name: 'todomvc/list', file: 'list.js' } ] @@ -82,7 +77,7 @@ for (const { name, file } of CONTRACTS) { if (existsSync(pinnedSource) && !source.equals(await readFile(pinnedSource))) { console.error( `src/contracts/${file} changed but the version is still ${VERSION}.\n` + - 'Bump "version" in package.json, or delete data/ and contracts/ to start fresh.' + 'Bump "version" in package.json, or delete data/, contracts/ and chelonia.json to start fresh.' ) process.exit(1) } diff --git a/scripts/chel.ts b/scripts/chel.ts index db2b148..a8ca096 100644 --- a/scripts/chel.ts +++ b/scripts/chel.ts @@ -1,5 +1,4 @@ import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' import process from 'node:process' @@ -7,10 +6,9 @@ import { fileURLToPath } from 'node:url' const require = createRequire(import.meta.url) -// Take the entry point from the package's own `bin` field instead of assuming -// where the file lives. Running it cannot be left to PATH or npx: npm creates -// no node_modules/.bin/chel shim for @chelonia/cli 3.4.0, even though the -// package declares one. +// The entry point comes from the package's own `bin` field rather than PATH, +// so this also works when a script is run with plain `node`, where +// node_modules/.bin is not on PATH. const manifestPath = require.resolve('@chelonia/cli/package.json') const { bin } = require(manifestPath) const CHEL_BIN = path.join( @@ -18,39 +16,9 @@ const CHEL_BIN = path.join( typeof bin === 'string' ? bin : bin.chel ) -// TODO: BEGIN REMOVEME (okTurtles/chel#150) -// The @chelonia/cli 3.4.0 binary is compiled with `--allow-write=./`, so it -// cannot write Deno's plug cache to fetch the SQLite3 library and every command -// fails with "Failed to load SQLite3 Dynamic Library". Pointing -// DENO_SQLITE_PATH at the system library skips the download. Fixed by -// okTurtles/chel#162, so this block goes once a release with it is out. -// -// These paths are conventional, not guaranteed, so on Linux take the first one -// that is actually there. On macOS the system libraries live in the dyld shared -// cache and there is no file to stat, so the path is used as given. -// DENO_SQLITE_PATH from the environment always wins. -const SYSTEM_SQLITE: Record = { - darwin: ['/usr/lib/libsqlite3.dylib'], - linux: [ - `/usr/lib/${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}-linux-gnu/libsqlite3.so.0`, - '/usr/lib64/libsqlite3.so.0', - '/usr/lib/libsqlite3.so.0' - ] -} - -const candidates = SYSTEM_SQLITE[process.platform] ?? [] -const found = process.platform === 'darwin' - ? candidates[0] - : candidates.find((p: string) => existsSync(p)) -const DENO_SQLITE_PATH = process.env.DENO_SQLITE_PATH ?? found ?? '' -// TODO: END REMOVEME (okTurtles/chel#150) - export function chel (args: string[]): void { const { status, signal } = spawnSync(process.execPath, [CHEL_BIN, ...args], { - stdio: 'inherit', - // TODO: BEGIN REMOVEME (okTurtles/chel#150) - env: { ...process.env, DENO_SQLITE_PATH } - // TODO: END REMOVEME (okTurtles/chel#150) + stdio: 'inherit' }) if (status !== 0) { throw new Error(`chel ${args.join(' ')} exited with ${status ?? signal}`) diff --git a/src/chelonia/auth.ts b/src/chelonia/auth.ts index 843ea97..7bca435 100644 --- a/src/chelonia/auth.ts +++ b/src/chelonia/auth.ts @@ -30,6 +30,7 @@ import { keygen, serializeKey } from '@chelonia/crypto' +import { ChelErrorUnexpectedHttpResponseCode } from '@chelonia/lib/errors' import type { ChelContractState } from '@chelonia/lib/types' import { API_URL, CONTRACT_NAME } from './config.ts' import { AuthError } from './errors.ts' @@ -169,16 +170,21 @@ async function retrieveSalt (identityContractID: ContractID, password: string): return contractSalt } -// TODO: BEGIN REMOVEME (okTurtles/libcheloniajs#90) -// Replaced by `chelonia/out/nameToContractID` once a @chelonia/lib release has -// it. The call in login() changes with it. -async function lookupUsername (username: string): Promise { - const response = await send(`/name/${encodeURIComponent(username)}`) - if (response.status === 404) return null - if (!response.ok) throw new AuthError(`Username lookup failed: ${response.status}`) - return response.text() +// The lookup is @chelonia/lib's, and it answers null for a name nobody has. +// What stays here is telling a request that never got an answer apart from +// one the server turned down, the same way `send` does. +async function lookupUsername (username: string): Promise { + try { + return await sbp('chelonia/out/nameToContractID', username) + } catch (e) { + if (e instanceof TypeError) { + throw new AuthError('Could not reach the server. Check your connection.', { + cause: e, exact: true + }) + } + throw new AuthError('Username lookup failed.', { cause: e }) + } } -// TODO: END REMOVEME (okTurtles/libcheloniajs#90) export async function signup ({ username, password }: Credentials): Promise { assertUsername(username) @@ -284,13 +290,15 @@ export async function signup ({ username, password }: Credentials): Promise boolean = () => false const queuedActions = (): QueuedAction[] => sbp('chelonia.persistentActions/status') export function setupOfflineQueue (): void { - ensureRandomUUID() keepQueueInLocalStorage() sbp('chelonia.persistentActions/configure', { databaseKey: QUEUE_KEY, @@ -64,24 +63,6 @@ export function setupOfflineQueue (): void { }) } -// TODO: BEGIN REMOVEME (okTurtles/libcheloniajs#100) -// PersistentAction ids come from crypto.randomUUID, which browsers only -// provide on https and localhost, so the first queued write throws when the -// demo is opened over the LAN. The lib does this itself now, so this goes with -// the next release. -function ensureRandomUUID (): void { - if (typeof crypto.randomUUID === 'function') return - ;(crypto as { randomUUID: () => `${string}-${string}-${string}-${string}-${string}` }).randomUUID = () => { - const bytes = crypto.getRandomValues(new Uint8Array(16)) - bytes[6] = (bytes[6]! & 0x0f) | 0x40 - bytes[8] = (bytes[8]! & 0x3f) | 0x80 - const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') - return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20)] - .join('-') as `${string}-${string}-${string}-${string}-${string}` - } -} -// TODO: END REMOVEME (okTurtles/libcheloniajs#100) - // chelonia.db is an in-memory map in this app, and the queue has to outlive a // reload, so this one key goes to localStorage instead. // diff --git a/src/contracts/identity.js b/src/contracts/identity.js index c1adc42..bff3654 100644 --- a/src/contracts/identity.js +++ b/src/contracts/identity.js @@ -6,26 +6,19 @@ // It is almost empty on purpose. Todos live in a KV slot, not in actions. What // the contract provides is what KV cannot: an object on the server that owns // the keys and gives `/kv/:contractID/:key` its scope. -// -// TODO: BEGIN REMOVEME (okTurtles/chel#160) -// The name has to be `gi.contracts/identity`, here and on the actions below. -// chel's POST /event only accepts a contract created without an account to -// bill it to when the manifest name is that, and only registers a username for -// a contract of that type. See src/serve/routes.ts in okTurtles/chel. -// TODO: END REMOVEME (okTurtles/chel#160) // Encrypted data on the wire is a `[keyId, ciphertext]` pair. The check is the // library's own: Chelonia gives the sandbox a `require` that resolves the // modules the app lists in `contracts.defaults.modules`, so there is no second -// copy of the rule here. See src/chelonia/config.js. +// copy of the rule here. See src/chelonia/config.ts. const { isRawEncryptedData } = require('@chelonia/lib/encryptedData') sbp('chelonia/defineContract', { - name: 'gi.contracts/identity', + name: 'todomvc/identity', actions: { // The initial action, published with OP_CONTRACT by // chelonia/out/registerContract. Its name is the contract name. - 'gi.contracts/identity': { + 'todomvc/identity': { validate (data) { if (typeof data?.attributes?.username !== 'string') { throw new TypeError('attributes.username must be a string') @@ -43,7 +36,7 @@ sbp('chelonia/defineContract', { }, // The token is encrypted with a password-derived key, so a password // change publishes it again under the new one. - 'gi.contracts/identity/setDeletionToken': { + 'todomvc/identity/setDeletionToken': { validate (data) { if (!isRawEncryptedData(data?.encryptedDeletionToken)) { throw new TypeError('encryptedDeletionToken must be encrypted data') diff --git a/test/e2e/chelonia.spec.ts b/test/e2e/chelonia.spec.ts index e1723a8..98425a3 100644 --- a/test/e2e/chelonia.spec.ts +++ b/test/e2e/chelonia.spec.ts @@ -13,6 +13,20 @@ test('a session survives a reload', async ({ page }) => { await expect(titles(page)).toHaveText(['survive a reload']) }) +// Making a list adds to the account's contract, and in Chelonia's full mode +// that needs a message log the browser does not keep across a reload. So this +// is what breaks if the app stops running as a lightweight client. +test('a list made after a reload is saved', async ({ page }) => { + await signup(page) + await page.reload() + await expect(page.locator('.list-tabs button')).toHaveText(['My todos']) + + await page.getByLabel('New list').fill('made after a reload') + await page.getByRole('button', { name: 'Add list' }).click() + await expect(page.locator('.list-tabs button')).toHaveText(['My todos', 'made after a reload']) + await expect(page.locator('.list-error')).toBeHidden() +}) + test('logging out clears the browser, logging back in recovers the todos', async ({ page }) => { const username = await signup(page) await addTodo(page, 'recovered from the contract') @@ -92,6 +106,26 @@ test('a taken username is reported as taken', async ({ page }) => { await expect(page.locator('.auth-error')).toHaveText('That username is already taken.') }) +// The publish error carries the HTTP status on `cause`, so signup can say why +// the server turned it down. chel answers these with a plain-text body. +for (const [status, message] of [ + [403, 'Signups are disabled on this server.'], + [409, 'That username is already taken.'], + [429, 'Too many signups from this network. Try again in a while.'] +] as const) { + test(`a signup refused with ${status} says why`, async ({ page }) => { + await page.goto('/app/') + await page.getByRole('button', { name: 'Create an account' }).click() + await page.route('**/event', (route) => route.request().method() === 'POST' + ? route.fulfill({ status, contentType: 'text/plain', body: 'refused' }) + : route.continue()) + await page.getByLabel('Username').fill(newUsername()) + await page.getByLabel('Password').fill(PASSWORD) + await page.getByRole('button', { name: 'Create account' }).click() + await expect(page.locator('.auth-error')).toHaveText(message) + }) +} + test('a username the server would reject is caught before any request', async ({ page }) => { await page.goto('/app/') await page.getByRole('button', { name: 'Create an account' }).click() diff --git a/vite.config.ts b/vite.config.ts index c099671..d1f8d98 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,14 +21,7 @@ export default defineConfig(({ mode }) => ({ // @chelonia/lib reads process.env at module scope. Replacing the whole // object avoids having to track which flags it reads. 'process.env': JSON.stringify({ - NODE_ENV: mode === 'production' ? 'production' : 'development', - // Without this, Chelonia keeps its own copy of every contract's message - // log in `chelonia.db`, which this app leaves as the default in-memory - // map. The saved state survives a reload but that map does not, so the - // first action after a reload fails with "No latest HEAD". An app that - // wants the full mode has to give Chelonia a `chelonia.db` backed by - // something durable, like IndexedDB. - LIGHTWEIGHT_CLIENT: 'true' + NODE_ENV: mode === 'production' ? 'production' : 'development' }) } }))