Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import type { FastifyInstance, FastifyRequest } from 'fastify';
import type { PublicConfig, VoiceAssignment } from '@wellformed/shared';
import { config } from '../config.js';
import { getWorkspaceBySlug } from '../control/workspaces.js';
import { emailEnabled } from '../mail/mailer.js';
import { LoginThrottle } from '../auth/throttle.js';
import { isDeclaredBot } from '../voice/bots.js';
import { cachedFit } from '../voice/fit.js';
import { drawPage } from '../voice/select.js';
import { markEngaged, recordImpression } from '../voice/store.js';
import { voiceIdParamSchema } from './schemas.js';
/**
* A ceiling on impression rows one address can create, not a security control.
* Deliberately generous: a person reloading a page is not an attack, and the
* consequence of tripping it is a page that still renders but is not measured.
*/
export const voiceThrottle = new LoginThrottle(60, 5 * 60_000, 5 * 60_000);
/**
* What this install tells a visitor with no session.
*
* Shared between the /api/public/config route and the server-rendered
* marketing page, which needs the same answers before any JavaScript runs.
* The demos come back as a list: which one to link is the page's decision,
* since only the page knows what language it is speaking.
*/
export function composePublicConfig(app: FastifyInstance): PublicConfig {
// Resolved rather than echoed: a slug naming a workspace that was renamed
// away or deleted would otherwise become a link to a 404 on the home page.
const demos = config.demoSlugs.flatMap((slug) => {
const workspace = getWorkspaceBySlug(app.control, slug);
return workspace && workspace.status === 'active'
? [{ slug: workspace.slug, name: workspace.name, currency: workspace.currency, locale: workspace.locale }]
: [];
});
return {
signupsOpen: config.signupsOpen,
emailEnabled: emailEnabled(),
metered: config.metered,
demos,
};
}
/**
* Draw the selling points one visit gets, and record the impression when the
* visit deserves one.
*
* A declared bot gets a page but an empty id — composed, not counted, and
* with no id there is nothing for it to beat against later. The same shape
* answers the rate limit, so the client needs only one rule: no id, no beat.
*/
export function assignVoice(app: FastifyInstance, request: FastifyRequest): VoiceAssignment {
const items = drawPage(cachedFit(app.control));
if (isDeclaredBot(request.headers['user-agent'])) return { id: '', items };
if (voiceThrottle.check(`ip:${request.ip}`) > 0) return { id: '', items };
voiceThrottle.fail(`ip:${request.ip}`);
return { id: recordImpression(app.control, items), items };
}
/**
* What this install is, before anyone signs in.
*
* Every one of these is a decision the *operator* made, and the client has no
* other way to learn it: whether to offer a signup link, whether "we sent you
* a link" is a true sentence here, whether there is a demo to point at, and
* what the plans cost. Nothing here names a workspace or an account, so it is
* safe to answer unauthenticated — which it must be, since the marketing page
* that reads it is the page you land on with no session at all.
*/
export async function publicRoutes(app: FastifyInstance): Promise<void> {
app.get('/api/public/config', async (): Promise<PublicConfig> => {
return composePublicConfig(app);
});
/**
* Which selling points this visit gets.
*
* Sampled here rather than in the browser so the pool's weights never leave
* the server and the recorded page is the one the server chose — an
* assignment the client could author would be one the client could stuff.
*
* Over the rate limit — or from a declared crawler — the page is still
* composed, just not recorded: the id comes back empty and the client knows
* not to post the beat. Refusing to answer would mean a landing page that
* fails because too many people asked for it, which is the wrong way round
* for a measurement to fail.
*/
app.get('/api/public/voice', async (request): Promise<VoiceAssignment> => {
return assignVoice(app, request);
});
/**
* A person, rather than a crawler, actually looked at it.
*
* Always 204, whether or not the id names anything. This says nothing back to
* its caller by design — it is the one endpoint on the site an anonymous
* stranger can address by id, and "that impression exists" is not a fact it
* needs to be able to confirm.
*
* A declared bot's beat is dropped here too: Google's renderer runs timers,
* so a crawler can pass the four-second dwell without a person existing.
*/
app.post('/api/public/voice/:id/seen', async (request, reply) => {
const { id } = voiceIdParamSchema.parse(request.params);
if (!isDeclaredBot(request.headers['user-agent'])) markEngaged(app.control, id);
reply.code(204);
return null;
});
/**
* The one crawler policy this install has, stated where crawlers look.
*
* Hosted, the landing pages are the product's shop window and welcome; a
* self-hosted install is somebody's private books behind a login, and its
* default is to ask not to be indexed. Served here rather than as a file so
* the answer can depend on which of the two this is, and so the sitemap can
* carry the operator's real origin.
*/
app.get('/robots.txt', async (request, reply) => {
reply.type('text/plain');
if (!config.metered) return 'User-agent: *\nDisallow: /\n';
return `User-agent: *\nAllow: /\n\nSitemap: ${config.publicUrl}/sitemap.xml\n`;
});
app.get('/sitemap.xml', async (request, reply) => {
if (!config.metered) {
reply.code(404).type('text/plain');
return 'This install asks not to be indexed.\n';
}
const pages = ['/', '/en', '/nl'];
reply.type('application/xml');
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...pages.map((page) => ` <url><loc>${config.publicUrl}${page === '/' ? '/' : page}</loc></url>`),
'</urlset>',
'',
].join('\n');
});
}