Files
a/site/checkout.js
zeekay 15a3c4c1dc a.hanzo.ai: harden loader key-guard + kill checkout price-forgery (hand red-review fixes)
RED (manual, agent hit the session limit): (1) secret-key guard /^(hk-|sk-)/ was
case-SENSITIVE and missed fw_ — HK-/SK-/fw_ secrets slipped past into the browser.
Now /^(hk-|sk-|fw_)/i across chat/analytics/checkout (keys already .trim()'d, so no
space bypass). (2) checkout sent a client data-price as unitPrice even for a slug'd
item → a forged data-price='0.01' could set the charge. Now unitPrice is sent ONLY
for a slug-LESS ad-hoc amount; any slug'd item is repriced server-side from the
catalog (forgery-proof). XSS verified PASS separately (ask.js renders model output
via textContent; the sole innerHTML is a static glyph).
2026-07-24 23:05:05 -07:00

81 lines
4.1 KiB
JavaScript

/*
* a.hanzo.ai/checkout — <script src=".../checkout" data-key="pk_..."></script>
* Loads [email protected] (window.HanzoCheckout), creates a client, exposes
* window.hanzoCheckout. Auto-binds any [data-hanzo-checkout] button for a
* zero-JS hosted checkout — card fields live on Hanzo's page, never the host DOM:
* <button data-hanzo-checkout data-slug="tee" data-name="Tee" data-price="29">Buy</button>
* data-key is a publishable pk_ key — never a secret. data-price is in dollars.
* Button opts: data-qty, data-currency, data-success, data-cancel. Script opts:
* data-host, data-currency, data-theme, data-color. Never throws into the page.
*/
(function () {
'use strict';
if (window.__hanzoCheckout) return;
var self = document.currentScript;
if (!self) return;
window.__hanzoCheckout = true;
var origin = new URL(self.src, location.href).origin;
var d = self.dataset || {};
var key = (d.key || '').trim();
if (!key) {
console.warn('[hanzo/checkout] missing data-key — add data-key="pk_…" (a publishable key). Not loaded.');
return;
}
if (/^(hk-|sk-|fw_)/i.test(key)) {
console.error('[hanzo/checkout] data-key looks like a SECRET key — never expose hk-/sk-/fw_ keys in a browser. Use a publishable pk_ key. Aborted.');
return;
}
var host = (d.host || 'https://api.hanzo.ai').replace(/\/+$/, '');
var currency = d.currency || 'USD';
var s = document.createElement('script');
s.async = true;
s.src = origin + '/vendor/checkout.global.js';
s.onload = function () {
try {
var C = window.HanzoCheckout;
if (!C || !C.create) { console.warn('[hanzo/checkout] checkout build loaded without HanzoCheckout.create.'); return; }
var client = C.create({ apiKey: key, baseUrl: host, currency: currency, appearance: { theme: d.theme || 'light', primaryColor: d.color || '#000' } });
window.hanzoCheckout = client; // custom carts: client.createSession(...).then(client.redirectToCheckout)
} catch (e) {
console.warn('[hanzo/checkout] init failed:', e && e.message);
}
};
s.onerror = function () { console.warn('[hanzo/checkout] failed to load checkout build from ' + origin + '/vendor/checkout.global.js'); };
(document.head || document.documentElement).appendChild(s);
// Zero-JS drop-in: one delegated listener opens a hosted session for a tagged
// button. Survives dynamically-added buttons; app-owned carts ignore this and
// drive window.hanzoCheckout directly.
document.addEventListener('click', function (ev) {
var t = ev.target && ev.target.closest ? ev.target.closest('[data-hanzo-checkout]') : null;
if (!t) return;
var client = window.hanzoCheckout;
if (!client) { console.warn('[hanzo/checkout] not ready yet.'); return; }
var b = t.dataset || {};
var price = Number(b.price);
if (!b.slug && !b.name) { console.warn('[hanzo/checkout] [data-hanzo-checkout] needs data-slug or data-name.'); return; }
ev.preventDefault();
var item = { productSlug: b.slug, id: b.slug || b.name, name: b.name || b.slug, quantity: Number(b.qty) || 1 };
// A slug'd item is repriced server-side from the catalog — never send a client
// data-price for it (would let a forged data-price="0.01" set the charge). The
// client price is honored ONLY for a slug-less ad-hoc amount, where the price
// IS the request (custom "pay this" with no catalog entry to reprice against).
if (!b.slug && !isNaN(price)) item.unitPrice = Math.round(price * 100);
var here = location.origin + location.pathname;
try {
client.createSession({
lineItems: [item],
currency: b.currency || currency,
successUrl: b.success || (here + '?checkout=success'),
cancelUrl: b.cancel || (here + '?checkout=cancel'),
metadata: { source: location.hostname }
}).then(function (session) { return client.redirectToCheckout(session); })
.catch(function (e) { console.warn('[hanzo/checkout] session failed:', e && (e.status || e.message)); });
} catch (e) { console.warn('[hanzo/checkout] session error:', e && e.message); }
}, true);
})();