diff --git a/apps/web/package.json b/apps/web/package.json index 7d57e1e..3480285 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@rssamplifier/web", - "version": "0.2.1", + "version": "0.2.2", "private": true, "type": "module", "scripts": { @@ -11,7 +11,7 @@ }, "dependencies": { "@logicsrc/openaccess": "^0.3.1", - "@profullstack/contact-form": "0.2.0", + "@profullstack/contact-form": "0.3.0", "@profullstack/footer": "latest", "@profullstack/leaderboard": "^0.3.1", "@profullstack/notifications": "^0.1.4", diff --git a/apps/web/src/app/sales/page.jsx b/apps/web/src/app/sales/page.jsx index 8b456da..df2c6b1 100644 --- a/apps/web/src/app/sales/page.jsx +++ b/apps/web/src/app/sales/page.jsx @@ -5,7 +5,7 @@ import { corpusFigures } from '../../lib/corpus.js'; import { latestClosedWindow } from '../../lib/datasetWindow.js'; import Toolbar from '../Toolbar.jsx'; import { ContactForm } from '@profullstack/contact-form/react'; -import { formErrors, salesContact } from '../../lib/salesContact.js'; +import { salesContact } from '../../lib/salesContact.js'; export const metadata = { title: 'Training data', @@ -203,7 +203,8 @@ export default async function SalesPage({ searchParams }) { ) : null} {error ? (
- {errorMessage(error)} + {/* Rebuilt from the codes a plain form post comes back with; the URL never carries text. */} + {salesContact.errorMessage(params)}
) : null}@@ -233,25 +234,6 @@ export default async function SalesPage({ searchParams }) { ); } -/** - * @param {string} code - * @returns {string} - */ -function errorMessage(code) { - // A message from the form's own checks, sent back by a plain form post. - if (formErrors().has(code)) return code; - switch (code) { - case 'bad-email': - return 'That does not look like an email address — we would have no way to reply.'; - case 'no-use-case': - return 'Please say what you want the corpus for. It is the field that decides what we can offer.'; - case 'too-many': - return 'That is several enquiries in an hour. Email hello@rssamplifier.com directly and we will pick it up there.'; - default: - return 'That did not send. Try again, or email hello@rssamplifier.com.'; - } -} - /** * @param {unknown} n * @returns {string} diff --git a/apps/web/src/lib/salesContact.js b/apps/web/src/lib/salesContact.js index 6ef22ae..49374b9 100644 --- a/apps/web/src/lib/salesContact.js +++ b/apps/web/src/lib/salesContact.js @@ -49,8 +49,8 @@ export const MAX_PER_WINDOW = 3; export const TOO_MANY = 'That is several enquiries in an hour. Email hello@rssamplifier.com directly and we will pick it up there.'; -export const NO_USE_CASE = - 'Please say what you want the corpus for — it is the field that decides what we can offer.'; +/** What any failure the sender cannot fix says, instead of the error itself. */ +export const DID_NOT_SEND = 'That did not send. Try again, or email hello@rssamplifier.com.'; /** The address a request comes from, the way every other limit on this site reads it. */ const ipOf = (req) => clientIp({ headers: req.headers }) || null; @@ -111,8 +111,7 @@ export function makeSalesContact(deps = {}) { rows: 5, maxLength: 4000, required: true, - pattern: '^[\\s\\S]{10,}$', - patternMessage: NO_USE_CASE, + minLength: 10, help: 'The last box is the one that decides what we can offer, so it is the one worth writing. Rough is fine; vague costs us both a round trip.', }, ], @@ -153,30 +152,10 @@ export function makeSalesContact(deps = {}) { 'Thank you — that is recorded and someone will reply to the address you gave.', responses: { limited: { body: { ok: false, error: TOO_MANY }, status: 429 }, + error: { body: { ok: false, error: DID_NOT_SEND }, status: 500 }, }, }); } export const salesContact = makeSalesContact(); -/** - * Every message a plain form post (JavaScript off) can be sent back to /sales - * with in `?error=`. The page shows one of these or a generic line, never - * whatever text a link happens to carry. - */ -export function formErrors(form = salesContact) { - const out = new Set([ - TOO_MANY, - 'That took too long. Please send it again.', - 'Too many messages. Please try again later.', - 'Invalid submission', - ]); - for (const q of form.questions) { - if (q.type === 'section') continue; - out.add(`${q.label} is required`); - out.add(`${q.label} is too long (${q.maxLength} characters at most)`); - if (q.type === 'email') out.add(`${q.label} must be a valid email address`); - if (q.patternMessage) out.add(q.patternMessage); - } - return out; -} diff --git a/apps/web/test/sales-contact.test.js b/apps/web/test/sales-contact.test.js index b10c758..4ae8231 100644 --- a/apps/web/test/sales-contact.test.js +++ b/apps/web/test/sales-contact.test.js @@ -3,7 +3,7 @@ import { test } from 'node:test'; import { createFormGuard } from '@profullstack/contact-form'; -import { MAX_PER_WINDOW, NO_USE_CASE, TOO_MANY, formErrors, makeSalesContact } from '../src/lib/salesContact.js'; +import { MAX_PER_WINDOW, TOO_MANY, makeSalesContact } from '../src/lib/salesContact.js'; const guard = createFormGuard({ secret: 'test-secret-not-used-anywhere-else', binding: 'rssamplifier:sales' }); @@ -92,11 +92,14 @@ test('a JSON caller may send useCase', async () => { assert.equal(rows[0].useCase, use_case); }); -test('a use case under ten characters is refused with the old wording', async () => { +test('a use case under ten characters is refused', async () => { const { form, rows } = harness(); const res = await form.POST(json({ ...answers, use_case: 'data', fg_token: await token() })); assert.equal(res.status, 400); - assert.deepEqual(await res.json(), { ok: false, error: NO_USE_CASE }); + assert.deepEqual(await res.json(), { + ok: false, + error: 'What do you want the corpus for? is too short (10 characters at least)', + }); assert.equal(rows.length, 0); }); @@ -121,5 +124,18 @@ test('a plain form post is sent back to the form, with a message the page will s const location = new URL(res.headers.get('location'), 'http://localhost'); assert.equal(location.pathname, '/sales'); assert.equal(location.hash, '#enquire'); - assert.ok(formErrors(form).has(location.searchParams.get('error'))); + // Codes, never text: the page rebuilds the message from them. + assert.equal(location.searchParams.get('error'), 'invalid'); + assert.equal(location.searchParams.get('field'), 'use_case'); + assert.equal( + form.errorMessage(location.searchParams), + 'What do you want the corpus for? is too short (10 characters at least)', + ); +}); + +test('a crafted ?error= shows the form\'s own wording, never the link\'s text', () => { + const { form } = harness(); + assert.equal(form.errorMessage({ error: 'Send your password to evil.example' }), form.errorMessage({ error: 'error' })); + assert.equal(form.errorMessage({ error: 'limited' }), TOO_MANY); + assert.equal(form.errorMessage({}), null); }); diff --git a/bun.lock b/bun.lock index 42b8501..d4de123 100644 --- a/bun.lock +++ b/bun.lock @@ -35,10 +35,10 @@ }, "apps/web": { "name": "@rssamplifier/web", - "version": "0.2.1", + "version": "0.2.2", "dependencies": { "@logicsrc/openaccess": "^0.3.1", - "@profullstack/contact-form": "0.2.0", + "@profullstack/contact-form": "0.3.0", "@profullstack/footer": "latest", "@profullstack/leaderboard": "^0.3.1", "@profullstack/notifications": "^0.1.4", @@ -355,7 +355,7 @@ "@peculiar/x509": ["@peculiar/x509@2.2.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.11.0", "@peculiar/asn1-csr": "^2.11.0", "@peculiar/asn1-ecc": "^2.11.0", "@peculiar/asn1-pkcs9": "^2.11.0", "@peculiar/asn1-rsa": "^2.11.0", "@peculiar/asn1-schema": "^2.11.0", "@peculiar/asn1-x509": "^2.11.0", "@peculiar/asn1-x509-post-quantum": "^2.11.0", "pvtsutils": "^1.3.6", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-adpW58ySPQMOUPv8pTKT3RIyCFqfUFt8X05ukPdtkQToXDVr8dMtxuenik1yx8tNFTbpApWsM/Hi5SzlIx48+Q=="], - "@profullstack/contact-form": ["@profullstack/contact-form@0.2.0", "", { "dependencies": { "@profullstack/emailer": "^1.0.3", "@profullstack/form-guard": "^0.3.0" }, "peerDependencies": { "react": ">=18.0.0" }, "optionalPeers": ["react"] }, "sha512-J2SSQo0qSYOAESe1jbOtIkAqimW+6/8EehOANVaXAq/DbMFCPfhKk0iH+g5lmrKHTG1yqs5a7o4nmrvt/WtiwA=="], + "@profullstack/contact-form": ["@profullstack/contact-form@0.3.0", "", { "dependencies": { "@profullstack/emailer": "^1.0.3", "@profullstack/form-guard": "^0.3.0" }, "peerDependencies": { "react": ">=18.0.0" }, "optionalPeers": ["react"] }, "sha512-62O5dj6tp6y76hfLPkTQG5MALRT1TkNg/T2FhZjU4QviKf7/ft5GULaKSPzgaR938oHk8JSmPC/JiahTfrdiUA=="], "@profullstack/emailer": ["@profullstack/emailer@1.0.3", "", {}, "sha512-VyAtCcfnSI6LOwJvWogCr3ZNmEsaZw+iuWrqnosLsdcKsbRyCrCmXigMNZJZEjlgYlR+lK//PwNPG+xtCFvWAw=="], diff --git a/package.json b/package.json index 61bdd42..2b98948 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rssamplifier.com", - "version": "0.2.1", + "version": "0.2.2", "private": true, "type": "module", "description": "Agent-first public RSS directory. Submit a URL, a list or an OPML file; every blog gets its own page.",