Skip to content

How the Punchline Stays Hidden

sarangshahane edited this page Aug 25, 2026 · 1 revision

How the Punchline Stays Hidden

The obvious way to build this widget is to send the question and its answer to the browser together and hide the answer with CSS. Anyone curious enough to press F12 would find it in seconds.

This plugin does not do that. Here is what it does instead, in plain terms.

The punchline never enters the page

When the widget asks for a question, the server generates all four parts — question, hint, category, punchline — and stores them together on the server. The response sent to the browser deliberately includes only three of them. The punchline is left behind.

So "view source" and the browser's network inspector show a question and a hint, and nothing else. There is nothing to uncover, because the answer was never sent.

It is released only on a POST, and only once

To get the punchline, the browser has to submit an answer. That is a POST request, and it is the only route to the punchline. There is no way to fetch it with a plain URL — no GET endpoint returns it, and requesting the endpoints with GET returns a 404 rather than data.

Once released, the question is marked as answered. Submitting again returns "This question has already been answered" rather than the punchline a second time.

The question expires

A generated question lives for ten minutes. After that it is gone from the server and the token is worthless — the visitor is told it expired and can request a new one.

After a reveal, the record is kept for a further two minutes only, so that a double-click gets a clear "already answered" message rather than a confusing "expired" one, and then it is discarded.

The request is tied to the visitor who made it

When a question is created, the server records a salted fingerprint of the requester. When an answer comes back, the fingerprint is recomputed and compared. A mismatch returns "This question belongs to another visitor" instead of the punchline.

Both the question token and the widget token are 48-character random strings, checked for shape on every request.

What this protects against, and what it does not

It does protect against the casual route: reading the answer from the page, from the network tab, or by guessing a URL. That is the realistic threat for a joke widget, and it is properly closed.

It does not make the punchline a secret. Anyone can request a question and submit any answer to receive it — that is what the widget is for. The reveal is gated on making an attempt, not on being right, and the plugin never checks whether an answer is correct.

So the guarantee is: you cannot skip ahead without playing. It is not: only worthy people get the answer.

The supporting pieces

AI output is treated as untrusted. Everything the model returns is stripped of markup and length-checked server-side before storage, and rendered as text rather than HTML in the browser. A model that returned a <script> tag would have it removed, and what survived would be printed as visible characters rather than executed.

Credentials never reach the browser. The AI call is made from your server. The only data handed to the widget's JavaScript is the REST URL and the interface labels — no keys, no endpoints, no model names.

The endpoints are public on purpose. They have to be: the widget works for logged-out visitors with no login prompt. Instead of authentication, they are protected by short-lived tokens, client binding, POST-only reveal, and rate limiting. A missing nonce here is a design decision, not an oversight — adding one would break anonymous use.

Further reading

The repository's docs/security.md covers the same ground for a developer audience, including the specific token and hashing mechanics.

Clone this wiki locally