Security and data handling
What ChangeGamer's code does today, for procurement reviewers and security researchers. The site is static HTML served by Cloudflare; a Cloudflare Worker handles resource files, the API, the MCP server, checkout links and key pickup. Each section describes current behaviour, not a promise.
Reporting a vulnerability
The reporting contact is published in /.well-known/security.txt (RFC 9116): security@changegamer.ai. Preferred languages: en, de. The file is valid until .
Transport and headers
Every page and file served from the static assets carries these response headers (the
/* rule of the site's _headers file):
| Header | Value |
|---|---|
Strict-Transport-Security | max-age=31536000 |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | geolocation=(), camera=(), microphone=(), browsing-topics=() |
Content-Security-Policy | default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests |
Cross-Origin-Opener-Policy | same-origin |
Content-Security-Policy-Report-Only | require-trusted-types-for 'script'; trusted-types 'none' |
Responses the worker builds itself (payment-required answers, MCP, key pickup, the grader) carry
X-Content-Type-Options: nosniff; the HTML pages it renders also carry a
Content-Security-Policy without scripts and X-Frame-Options: DENY. The key pickup
page /key is sent with Referrer-Policy: no-referrer and
Cache-Control: no-store, and premium files delivered over HTTP for a key are sent
with Cache-Control: private, no-store.
The site's pages load scripts only from its own origin: /search.js on the search page
and /grader.js on the grader. Neither writes HTML strings into the page; both build
elements through DOM methods. The site's own code sets no cookies.
Payments
- Checkout runs on Stripe's hosted checkout page. Card data is entered there and does not pass through ChangeGamer's code.
- The fulfillment webhook accepts an event only with a valid
Stripe-Signatureheader: an HMAC-SHA256 over the timestamp and the raw request body, keyed with the endpoint's signing secret and compared with the Web Cryptoverifyfunction. Events whose timestamp is more than 300 seconds from the worker's clock are rejected. - A key is minted only for
checkout.session.completedorcheckout.session.async_payment_succeededevents with payment statuspaid. A later redelivery for a checkout session that already has a key does not mint a second one; two deliveries arriving at the same moment for a new session can each mint a key. - From the event, the webhook reads the checkout session id, payment status, payment-link id (which selects the tier), checkout email and, for subscriptions, the subscription id.
Access keys
- A key is
cg_followed by 40 hexadecimal characters: 160 bits from the Web Crypto random number generator (crypto.getRandomValues). - Clients send it as
Authorization: Bearer <key>orx-api-key: <key>. Keys are stored in plain text in Cloudflare KV: as the lookup key of their own record and as the value of the session, subscription and revocation records. - Keys from one-time purchases have no expiry in code. A key from an annual subscription is deleted when the worker receives Stripe's
customer.subscription.deletedevent, which requires that event to be enabled on the Stripe webhook endpoint. - The buyer picks the key up at
/keywith the Stripe checkout session id; that lookup record expires after 30 days.
What the site records
- With each access key: Cloudflare KV holds the checkout email, the Stripe checkout session id, the tier, the Stripe payment-link id (when present) and the creation time. The code sets no expiry on this record.
- For key pickup at /key: A record of the session id with the key, tier and creation time, which expires after 30 days.
- For annual subscriptions: A record of the Stripe subscription id with the key, without expiry. When the worker receives Stripe's customer.subscription.deleted event (if that event is enabled on the Stripe webhook endpoint), it deletes the key and this record and keeps a revocation record (subscription id, key, time).
- Per request: For requests the worker handles (for example resource and API fetches, guide pages, MCP calls, key pickup and checkout links), it writes an event to Cloudflare Workers Analytics Engine with the kind of request, the item requested, the outcome and the first 100 characters of the User-Agent header and, for offer-page views and checkout links, the host name of the referring site (no path or query). No IP address, email address or key is written to these events.
- Errors: when the worker hits an unexpected error it logs one line with the route pattern and the error name to Cloudflare Workers Logs. Per-request invocation logs are switched off.
- Rate limits:
POST /api/graderand the three large MCP results (get_corpuswithout a category,get_full_corpus,list_articleswithfull: true) pass the client IP address to Cloudflare's rate-limiting bindings to count requests; the worker does not write the address anywhere else. The analytics event for a grade holds the graded site's host name.