+
+
+
## Links
+- [Downloads](/terraphim-llm-proxy/download/)
- [Privacy policy](/terraphim-llm-proxy/privacy/)
- [Refund policy](/terraphim-llm-proxy/refunds/)
- [Support — GitHub issues](https://github.com/terraphim/terraphim-ai/issues)
diff --git a/content/terraphim-llm-proxy/download.md b/content/terraphim-llm-proxy/download.md
new file mode 100644
index 0000000..519eb50
--- /dev/null
+++ b/content/terraphim-llm-proxy/download.md
@@ -0,0 +1,48 @@
++++
+title = "Terraphim Proxy — Downloads"
+description = "Signed macOS releases of Terraphim Proxy for licensed customers."
+weight = 40
+template = "page.html"
+render = true
++++
+
+Signed macOS builds of Terraphim Proxy, served from the Terraphim Skills
+marketplace. Downloads are tied to the account you purchased with — sign
+in below with that account, then pick a release. Every artefact is listed
+with its SHA-256 so you can verify the download before opening it.
+
+
+
+
+ Purchase confirmed — your downloads are below.
+
+
+
+
Loading releases…
+
+
+
+
diff --git a/content/terraphim-llm-proxy/privacy.md b/content/terraphim-llm-proxy/privacy.md
index 5962bdb..c7fed01 100644
--- a/content/terraphim-llm-proxy/privacy.md
+++ b/content/terraphim-llm-proxy/privacy.md
@@ -1,7 +1,7 @@
+++
title = "Terraphim Proxy — Privacy Policy"
description = "Privacy policy for the Terraphim Proxy desktop application (cloud.terraphim.desktop)"
-date = 2026-10-22
+date = 2026-10-24
weight = 10
[taxonomies]
@@ -11,16 +11,21 @@ categories = ["Legal"]
toc = false
+++
-**Effective date: 22 October 2026.** Applies to the Terraphim Proxy macOS
+**Effective date: 24 October 2026.** Applies to the Terraphim Proxy macOS
application (`cloud.terraphim.desktop`), distributed via the Mac App
Store and direct download.
## The short version
-Terraphim Proxy collects **no data whatsoever**. There is no account system,
-no telemetry, no analytics, no crash reporting, and no third-party
-tracking SDK. Your credentials never leave your Mac except inside the
-requests you deliberately send to the LLM providers you configured.
+The Terraphim Proxy **application** collects **no data whatsoever**. There
+is no account system, no telemetry, no analytics, no crash reporting, and
+no third-party tracking SDK in the app. Your credentials never leave your
+Mac except inside the requests you deliberately send to the LLM providers
+you configured.
+
+Buying a direct download does create a small server-side record — kept by
+the Terraphim Skills marketplace, not by the app — described under
+*Purchases* below.
## What stays on your device
@@ -47,10 +52,20 @@ OpenAI, xAI, or any other provider you configure).
## Purchases
-The app is a one-time purchase processed entirely by Apple's App Store.
-Apple — not Terraphim Proxy — receives and processes your payment
-information. The app contains no in-app purchases, no subscription
-checks and no licence servers.
+**Mac App Store.** The app is a one-time purchase processed entirely by
+Apple's App Store. Apple — not Terraphim Proxy — receives and processes
+your payment information. The app contains no in-app purchases, no
+subscription checks and no licence servers.
+
+**Direct download.** Direct-download purchases are processed by Stripe
+via the Terraphim Skills marketplace (`api.terraphim-skills.md`). Stripe
+— not Terraphim — receives and processes your payment information. The
+marketplace stores the email address on your account, the fact of the
+purchase, and which releases you have downloaded; those records are used
+solely for entitlement checks and refund adjudication, as described in
+the [Refund policy](/terraphim-llm-proxy/refunds/). The application
+itself still collects nothing — this record lives entirely on the
+marketplace side.
## Permissions the app requests
diff --git a/static/js/proxy-buy.js b/static/js/proxy-buy.js
new file mode 100644
index 0000000..3355ceb
--- /dev/null
+++ b/static/js/proxy-buy.js
@@ -0,0 +1,51 @@
+/* Terraphim Proxy — buy button.
+ Wires #proxy-buy-btn on /terraphim-llm-proxy/ to the Terraphim Skills
+ marketplace checkout. Contract: GET /api/billing/checkout?product=…
+ with a Bearer JWT returns {"url": ""}. */
+(function () {
+ 'use strict';
+ var API = 'https://api.terraphim-skills.md';
+ var btn = document.getElementById('proxy-buy-btn');
+ var status = document.getElementById('proxy-buy-status');
+ if (!btn || !status) { return; }
+
+ function getToken() {
+ try { return localStorage.getItem('terraphim_token'); } catch (e) { return null; }
+ }
+
+ function showSignIn(note) {
+ status.innerHTML = note +
+ ' Sign in with GitHub, then try again.';
+ }
+
+ btn.addEventListener('click', function () {
+ var token = getToken();
+ if (!token) {
+ showSignIn('Purchasing needs a Terraphim account.');
+ return;
+ }
+ btn.disabled = true;
+ status.textContent = 'Preparing checkout…';
+ fetch(API + '/api/billing/checkout?product=terraphim-proxy', {
+ headers: { 'Authorization': 'Bearer ' + token }
+ })
+ .then(function (res) {
+ if (res.status === 401) {
+ showSignIn('Your session has expired —');
+ btn.disabled = false;
+ return null;
+ }
+ if (!res.ok) { throw new Error('checkout ' + res.status); }
+ return res.json();
+ })
+ .then(function (data) {
+ if (!data) { return; }
+ if (data.url) { window.location.href = data.url; return; }
+ throw new Error('checkout returned no url');
+ })
+ .catch(function () {
+ status.textContent = 'Checkout is unavailable right now — please try again later.';
+ btn.disabled = false;
+ });
+ });
+})();
diff --git a/static/js/proxy-download.js b/static/js/proxy-download.js
new file mode 100644
index 0000000..30a7f45
--- /dev/null
+++ b/static/js/proxy-download.js
@@ -0,0 +1,181 @@
+/* Terraphim Proxy — entitlement-gated release downloads.
+ Renders the public release catalogue on /terraphim-llm-proxy/download/
+ and gates artefact fetches behind a Bearer JWT; 402 means the account
+ has not purchased. Contract (api.terraphim-skills.md):
+ GET /api/products/terraphim-proxy/releases
+ -> [{version, asset_name, size_bytes, sha256, published_at}]
+ (a wrapped {"releases": [...]} shape is tolerated)
+ GET /api/products/terraphim-proxy/releases/{version}/{asset}
+ -> artefact bytes + X-SHA256 header; 402 {"error":"purchase_required"}. */
+(function () {
+ 'use strict';
+ var API = 'https://api.terraphim-skills.md';
+ var BUY_URL = '/terraphim-llm-proxy/#your-keys-your-costs';
+ var SIGNIN_URL = API + '/auth/oauth/github';
+
+ var banner = document.getElementById('proxy-dl-banner');
+ var notice = document.getElementById('proxy-dl-notice');
+ var wrap = document.getElementById('proxy-dl-wrap');
+ if (!notice || !wrap) { return; }
+
+ try {
+ if (new URLSearchParams(window.location.search).get('checkout') === 'success' && banner) {
+ banner.hidden = false;
+ }
+ } catch (e) { /* malformed query — ignore */ }
+
+ function getToken() {
+ try { return localStorage.getItem('terraphim_token'); } catch (e) { return null; }
+ }
+
+ function esc(s) {
+ return String(s === null || s === undefined ? '' : s)
+ .replace(/&/g, '&').replace(//g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''');
+ }
+
+ function humanSize(bytes) {
+ var n = Number(bytes);
+ if (!isFinite(n) || n < 0) { return '—'; }
+ var units = ['B', 'KB', 'MB', 'GB'];
+ var i = 0;
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
+ return (i === 0 ? String(n) : n.toFixed(1)) + ' ' + units[i];
+ }
+
+ function fmtDate(iso) {
+ var d = new Date(iso);
+ if (isNaN(d.getTime())) { return iso || '—'; }
+ return d.toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric' });
+ }
+
+ function signInPrompt() {
+ notice.innerHTML =
+ '
Releases are unavailable right now — please try again later.
';
+ });
+
+ function renderTable(releases) {
+ var hasToken = !!getToken();
+ var rows = [];
+ for (var i = 0; i < releases.length; i++) {
+ var r = releases[i] || {};
+ var sha = r.sha256 || '';
+ var shaCell = sha
+ ? '' + esc(sha.slice(0, 12)) + '…'
+ : '—';
+ var action = hasToken
+ ? ''
+ : '' +
+ ' Sign in';
+ rows.push(
+ '
' +
+ '
' + esc(r.version || '—') + '
' +
+ '
' + esc(r.asset_name || '—') + '
' +
+ '
' + esc(fmtDate(r.published_at)) + '
' +
+ '
' + esc(humanSize(r.size_bytes)) + '
' +
+ '
' + shaCell + '
' +
+ '
' + action + '
' +
+ '
'
+ );
+ }
+ wrap.innerHTML =
+ '
' +
+ '
Version
Asset
Released
Size
SHA-256
' +
+ '
' + rows.join('') + '
';
+ var btns = wrap.querySelectorAll('button[data-i]');
+ for (var j = 0; j < btns.length; j++) {
+ (function (btn) {
+ btn.addEventListener('click', function () {
+ downloadRelease(releases[Number(btn.getAttribute('data-i'))], btn);
+ });
+ })(btns[j]);
+ }
+ }
+
+ function downloadRelease(rel, btn) {
+ var token = getToken();
+ var rs = btn.parentNode.querySelector('.proxy-dl-row-status');
+ if (!token) { signInPrompt(); return; }
+ btn.disabled = true;
+ rs.innerHTML =
+ ' downloading…';
+ var url = API + '/api/products/terraphim-proxy/releases/' +
+ encodeURIComponent(rel.version) + '/' + encodeURIComponent(rel.asset_name);
+ fetch(url, { headers: { 'Authorization': 'Bearer ' + token } })
+ .then(function (res) {
+ if (res.status === 401) {
+ rs.innerHTML = '';
+ signInPrompt();
+ throw { handled: true };
+ }
+ if (res.status === 402) {
+ rs.innerHTML = '';
+ purchaseRequired();
+ throw { handled: true };
+ }
+ if (!res.ok) { throw new Error('download ' + res.status); }
+ var sha = res.headers.get('X-SHA256');
+ return res.blob().then(function (blob) { return { blob: blob, sha: sha }; });
+ })
+ .then(function (result) {
+ if (!result) { return; }
+ var objectUrl = URL.createObjectURL(result.blob);
+ var a = document.createElement('a');
+ a.href = objectUrl;
+ a.download = rel.asset_name || 'terraphim-proxy.dmg';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 5000);
+ rs.innerHTML = result.sha
+ ? ' ' +
+ 'Saved. SHA-256: ' + esc(result.sha) + ' ' +
+ '— verify with shasum -a 256'
+ : ' ' +
+ 'Saved ' + esc(rel.asset_name || 'artefact') + '.';
+ })
+ .catch(function (err) {
+ if (err && err.handled) { return; }
+ rs.innerHTML =
+ ' ' +
+ 'Download failed — please try again.';
+ })
+ .finally(function () { btn.disabled = false; });
+ }
+})();
From aaeebab9751b9d309169fafb030bb42727d4c101 Mon Sep 17 00:00:00 2001
From: Dr Alexander Mikhalev
Date: Sat, 24 Oct 2026 15:30:58 +0100
Subject: [PATCH 2/2] =?UTF-8?q?fix(site):=20review=20follow-ups=20?=
=?UTF-8?q?=E2=80=94=20OAuth=20return=5Fto,=20banner=20gating,=20refund=20?=
=?UTF-8?q?policy,=20JS=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
F1: sign-in links now carry ?return_to= so the
OAuth callback lands back on this origin with the JWT in the fragment;
both page scripts capture #token= (and ?token= for forward-compat)
into localStorage and history.replaceState it out of the address bar.
F3: the ?checkout=success banner only reads 'purchase confirmed' when a
token is present; signed-out visitors see neutral 'checkout completed —
sign in' wording — the parameter is trivially forgeable.
F4/F5: refund policy — short version now matches the table (14 days +
no download), both CONFIRM placeholders resolved ('effective upon launch
of direct-download sales', '3 business days'), and 'downloaded' is
defined as when our service first serves a release download request —
failed/interrupted delivery does not count, ambiguous cases resolved in
the customer's favour, cross-referenced from the table.
F6: pure helpers extracted to static/js/proxy-helpers.js (UMD-style —
window.ProxyHelpers in the browser, module.exports under node) with
node:test coverage in tests/js/proxy-helpers.test.js: escaping of
script/attribute payloads, wrapped vs unwrapped release shapes, URL
encoding, humanSize/fmtDate edge cases, token-in-location parsing and
hash stripping. 15 tests, 0 deps, no mocks.
Refs terraphim/terraphim-llm-proxy#508
---
content/terraphim-llm-proxy/_index.md | 1 +
content/terraphim-llm-proxy/download.md | 1 +
content/terraphim-llm-proxy/privacy.md | 5 +-
content/terraphim-llm-proxy/refunds.md | 32 +++---
static/js/proxy-buy.js | 40 +++++++-
static/js/proxy-download.js | 104 ++++++++++---------
static/js/proxy-helpers.js | 92 +++++++++++++++++
tests/js/proxy-helpers.test.js | 127 ++++++++++++++++++++++++
8 files changed, 338 insertions(+), 64 deletions(-)
create mode 100644 static/js/proxy-helpers.js
create mode 100644 tests/js/proxy-helpers.test.js
diff --git a/content/terraphim-llm-proxy/_index.md b/content/terraphim-llm-proxy/_index.md
index 8e571b2..bb606e1 100644
--- a/content/terraphim-llm-proxy/_index.md
+++ b/content/terraphim-llm-proxy/_index.md
@@ -65,6 +65,7 @@ requests you send to your providers.
.proxy-buy-status:empty { margin: 0; }
.proxy-buy-caption { font-size: 0.8rem; color: var(--text-dim); margin: 0.25rem 0 0; }
+
## Links
diff --git a/content/terraphim-llm-proxy/download.md b/content/terraphim-llm-proxy/download.md
index 519eb50..289ea21 100644
--- a/content/terraphim-llm-proxy/download.md
+++ b/content/terraphim-llm-proxy/download.md
@@ -45,4 +45,5 @@ with its SHA-256 so you can verify the download before opening it.
#proxy-download .proxy-dl-sha { font-size: 0.75rem; }
#proxy-download .proxy-dl-table td code { overflow-wrap: anywhere; }
+
diff --git a/content/terraphim-llm-proxy/privacy.md b/content/terraphim-llm-proxy/privacy.md
index c7fed01..e320576 100644
--- a/content/terraphim-llm-proxy/privacy.md
+++ b/content/terraphim-llm-proxy/privacy.md
@@ -61,8 +61,9 @@ subscription checks and no licence servers.
via the Terraphim Skills marketplace (`api.terraphim-skills.md`). Stripe
— not Terraphim — receives and processes your payment information. The
marketplace stores the email address on your account, the fact of the
-purchase, and which releases you have downloaded; those records are used
-solely for entitlement checks and refund adjudication, as described in
+purchase, and which release downloads it has served to your account;
+those records are used solely for entitlement checks and refund
+adjudication, as described in
the [Refund policy](/terraphim-llm-proxy/refunds/). The application
itself still collects nothing — this record lives entirely on the
marketplace side.
diff --git a/content/terraphim-llm-proxy/refunds.md b/content/terraphim-llm-proxy/refunds.md
index 15da1ec..49c6687 100644
--- a/content/terraphim-llm-proxy/refunds.md
+++ b/content/terraphim-llm-proxy/refunds.md
@@ -6,7 +6,7 @@ template = "page.html"
render = true
+++
-**Last updated:** [CONFIRM: set to the date direct-download sales open]
+**Effective upon launch of direct-download sales.**
This policy covers **direct downloads purchased on terraphim.ai**.
Purchases made through the Mac App Store are refunded by Apple under the
@@ -15,22 +15,23 @@ not Terraphim, decides those refunds.
## The short version
-**You can refund any time before you download a release. Once your
-account has downloaded a release, the sale is final — unless the proxy
-does not function.**
+**Within 14 days of purchase you can get a full refund provided no
+release has been downloaded. Once your account has downloaded a release,
+the sale is final — unless the proxy does not function.**
This is the same shape as Steam's rule for games: a refund is available
while the product is untouched, and the usage measurement is what makes
-the rule fair to enforce. Steam measures playtime; we measure the
-download event.
+the rule fair to enforce. Steam measures playtime; we measure the moment
+our service first serves the download.
## Why we do it this way
A signed application bundle cannot be returned once it is copied to your
Mac — there is no undo. The download is the honest line between
"bought but not taken" and "taken", and it is a line we can measure: the
-download service records which releases your account has downloaded, and
-that record is the only thing the refund rule consults.
+service records the first time it serves a release download request to
+your account, and that record is the only thing the refund rule
+consults.
We considered the alternative — a blanket 14-day window — and rejected
it, because it would let anyone buy, download every release, and refund.
@@ -46,6 +47,12 @@ else.
| **Any release downloaded**, proxy does not function | Full refund — see *When the proxy does not function* below |
| More than 14 days after purchase, no download | Not automatic. Ask anyway — see *Outside the rules* below. |
+*"Downloaded" is counted when our service begins delivering the release
+to your account — the request is recorded when delivery starts, not when
+the bytes finish. A failed or interrupted delivery does not count as a
+completed download for refund purposes (see *Downloads were broken*
+below), and where the record is ambiguous we treat it in your favour.*
+
Refunds go back to the original payment method through Stripe. Card
refunds usually appear within 5–10 business days, depending on your bank
— that part is outside our control.
@@ -79,7 +86,7 @@ Email **[engineering@terraphim.ai](mailto:engineering@terraphim.ai)**
with the email address on the account you purchased with. For a defect
claim, include the app version and macOS version.
-We aim to process refund requests within **[CONFIRM: 3 business days]**.
+We aim to process refund requests within **3 business days**.
## Outside the rules
@@ -96,9 +103,10 @@ is not closed. We will look seriously at:
## What we record
To make this policy enforceable we keep a server-side record, tied to
-your purchase account, of **which releases you downloaded and when**.
-That is the entire record — the app itself collects no telemetry and
-reports nothing back. Purchase and download records are covered in the
+your purchase account, of **when our service first serves a release
+download request to you** — the version, the asset and the time. That is
+the entire record — the app itself collects no telemetry and reports
+nothing back. Purchase and download records are covered in the
[Privacy policy](/terraphim-llm-proxy/privacy/).
## Your statutory rights
diff --git a/static/js/proxy-buy.js b/static/js/proxy-buy.js
index 3355ceb..ac7d73e 100644
--- a/static/js/proxy-buy.js
+++ b/static/js/proxy-buy.js
@@ -1,23 +1,53 @@
/* Terraphim Proxy — buy button.
Wires #proxy-buy-btn on /terraphim-llm-proxy/ to the Terraphim Skills
marketplace checkout. Contract: GET /api/billing/checkout?product=…
- with a Bearer JWT returns {"url": ""}. */
+ with a Bearer JWT returns {"url": ""}.
+
+ Sign-in is a round trip: the link carries ?return_to=, the
+ OAuth callback lands back here with the JWT in the fragment
+ (#token=), which captureToken() below moves into localStorage and
+ strips from the address bar. Requires /js/proxy-helpers.js loaded first. */
(function () {
'use strict';
- var API = 'https://api.terraphim-skills.md';
+ var H = window.ProxyHelpers;
var btn = document.getElementById('proxy-buy-btn');
var status = document.getElementById('proxy-buy-status');
- if (!btn || !status) { return; }
+ if (!H || !btn || !status) { return; }
+
+ /* Move a JWT handed back in the URL fragment (or, forward-compat, the
+ query) into localStorage, then scrub it from the address bar and
+ history — a JWT must never linger in either. */
+ function captureToken() {
+ var tok = H.tokenFromLocation(window.location);
+ if (!tok) { return; }
+ try { localStorage.setItem('terraphim_token', tok); } catch (e) { return; }
+ try {
+ var q = new URLSearchParams(window.location.search);
+ q.delete('token');
+ var qs = q.toString();
+ var hash = H.stripTokenFromHash(window.location.hash);
+ var clean = window.location.origin + window.location.pathname +
+ (qs ? '?' + qs : '') + (hash ? '#' + hash : '');
+ history.replaceState(null, '', clean);
+ } catch (e) { /* replaceState unavailable — token is stored anyway */ }
+ }
function getToken() {
try { return localStorage.getItem('terraphim_token'); } catch (e) { return null; }
}
+ function signInHref() {
+ var back = window.location.origin + window.location.pathname + window.location.search;
+ return H.signInUrl(back);
+ }
+
function showSignIn(note) {
status.innerHTML = note +
- ' Sign in with GitHub, then try again.';
+ ' Sign in with GitHub, then try again.';
}
+ captureToken();
+
btn.addEventListener('click', function () {
var token = getToken();
if (!token) {
@@ -26,7 +56,7 @@
}
btn.disabled = true;
status.textContent = 'Preparing checkout…';
- fetch(API + '/api/billing/checkout?product=terraphim-proxy', {
+ fetch(H.API + '/api/billing/checkout?product=terraphim-proxy', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(function (res) {
diff --git a/static/js/proxy-download.js b/static/js/proxy-download.js
index 30a7f45..66e1d4d 100644
--- a/static/js/proxy-download.js
+++ b/static/js/proxy-download.js
@@ -6,53 +6,69 @@
-> [{version, asset_name, size_bytes, sha256, published_at}]
(a wrapped {"releases": [...]} shape is tolerated)
GET /api/products/terraphim-proxy/releases/{version}/{asset}
- -> artefact bytes + X-SHA256 header; 402 {"error":"purchase_required"}. */
+ -> artefact bytes + X-SHA256 header; 402 {"error":"purchase_required"}.
+
+ Sign-in is a round trip: links carry ?return_to=, the OAuth
+ callback lands back here with the JWT in the fragment (#token=),
+ which captureToken() moves into localStorage and strips from the
+ address bar. Requires /js/proxy-helpers.js loaded first. */
(function () {
'use strict';
- var API = 'https://api.terraphim-skills.md';
- var BUY_URL = '/terraphim-llm-proxy/#your-keys-your-costs';
- var SIGNIN_URL = API + '/auth/oauth/github';
-
+ var H = window.ProxyHelpers;
var banner = document.getElementById('proxy-dl-banner');
var notice = document.getElementById('proxy-dl-notice');
var wrap = document.getElementById('proxy-dl-wrap');
- if (!notice || !wrap) { return; }
+ if (!H || !notice || !wrap) { return; }
- try {
- if (new URLSearchParams(window.location.search).get('checkout') === 'success' && banner) {
- banner.hidden = false;
- }
- } catch (e) { /* malformed query — ignore */ }
+ var BUY_URL = '/terraphim-llm-proxy/#your-keys-your-costs';
+
+ /* Move a JWT handed back in the URL fragment (or, forward-compat, the
+ query) into localStorage, then scrub it from the address bar and
+ history — a JWT must never linger in either. */
+ function captureToken() {
+ var tok = H.tokenFromLocation(window.location);
+ if (!tok) { return; }
+ try { localStorage.setItem('terraphim_token', tok); } catch (e) { return; }
+ try {
+ var q = new URLSearchParams(window.location.search);
+ q.delete('token');
+ var qs = q.toString();
+ var hash = H.stripTokenFromHash(window.location.hash);
+ var clean = window.location.origin + window.location.pathname +
+ (qs ? '?' + qs : '') + (hash ? '#' + hash : '');
+ history.replaceState(null, '', clean);
+ } catch (e) { /* replaceState unavailable — token is stored anyway */ }
+ }
function getToken() {
try { return localStorage.getItem('terraphim_token'); } catch (e) { return null; }
}
- function esc(s) {
- return String(s === null || s === undefined ? '' : s)
- .replace(/&/g, '&').replace(//g, '>')
- .replace(/"/g, '"').replace(/'/g, ''');
+ function signInHref() {
+ var back = window.location.origin + window.location.pathname + window.location.search;
+ return H.signInUrl(back);
}
- function humanSize(bytes) {
- var n = Number(bytes);
- if (!isFinite(n) || n < 0) { return '—'; }
- var units = ['B', 'KB', 'MB', 'GB'];
- var i = 0;
- while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
- return (i === 0 ? String(n) : n.toFixed(1)) + ' ' + units[i];
- }
+ captureToken();
- function fmtDate(iso) {
- var d = new Date(iso);
- if (isNaN(d.getTime())) { return iso || '—'; }
- return d.toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric' });
- }
+ /* ?checkout=success comes back from Stripe. Only claim the purchase is
+ confirmed when a token is present — anyone can forge the parameter,
+ so signed-out visitors get neutral wording instead. */
+ try {
+ if (new URLSearchParams(window.location.search).get('checkout') === 'success' && banner) {
+ if (!getToken()) {
+ banner.innerHTML =
+ ' ' +
+ 'Checkout completed — sign in below to confirm download access.';
+ }
+ banner.hidden = false;
+ }
+ } catch (e) { /* malformed query — ignore */ }
function signInPrompt() {
notice.innerHTML =
'