openvibers/openvibe.live Commits
Security: a PowerChat checkout ref (pcorder/pcsub/pcdon) only counts on the account its checkout routes to — a streamer tipping their own PowerChat with a crafted ref minted cashout Vibes; FedCM assertions and service/app tokens are refused as Live sessions
Changes
@@@ -45,13 +45,21 @@ loadNetworkPublicKey();* Verify a openvibe.network RS256 JWT token.* Returns decoded payload or null.*/++// A user session token carries neither `typ` (FedCM assertions: 5 minutes, minted for one origin, to be++// exchanged at Network, never a session) nor `actor_type` (service/app principal tokens). Both are++// signed with the same Network key, so the signature alone does not make a token a login.++function isUserSessionClaims(decoded) {++ return !!decoded && typeof decoded === 'object' && decoded.typ === undefined && decoded.actor_type === undefined;++}++function verifyToken(token) {if (!openvibeToolsPublicKey) return null;try {-- return jwt.verify(token, openvibeToolsPublicKey, {++ const decoded = jwt.verify(token, openvibeToolsPublicKey, {algorithms: ['RS256'],issuer: getNetworkIssuer(),});++ return isUserSessionClaims(decoded) ? decoded : null;} catch {return null;}@@@ -68,6 +76,7 @@ function verifyTokenWithReason(token) {algorithms: ['RS256'],issuer: getNetworkIssuer(),});++ if (!isUserSessionClaims(decoded)) return { ok: false, reason: 'not_a_session_token' };return { ok: true, decoded };} catch (err) {return { ok: false, reason: err.name, message: err.message };@@@ -300,12 +300,22 @@ function buildDonateLink(streamerUserId, donorUserId, { goalId = null } = {}) {// matches one of our prefixes. Returns true when the event was CONSUMED (the normal// donation pipeline for the receiving account must then be skipped), false to fall// through to plain donation handling.--function handleAttributedDonation(receivingUserId, data) {++// The checkout reference rides in the tip link and the buyer can edit it, so a reference only++// counts when the money landed where that kind of checkout sends it: purchases, site-routed++// donations and site-routed subscriptions on the SITE tips account, a direct subscription on++// the subscribed streamer's own account. Anything else (a streamer tipping their own PowerChat++// with `pcdon:<self>`, a viewer tipping themselves with `pcsub:<x>`) is an ordinary tip to the++// receiving account and falls through to normal handling.++function handleAttributedDonation(receivingUserId, data, { viaSiteAccount = receivingUserId == null } = {}) {const ref = String(data.appExternalRef || '');const usdCents = Math.max(0, Math.round(Number(data.amountUsdCents || 0)));try {// Buy Vibes ---------------------------------------------------------let m = ref.match(/^pcorder:(\d+)$/);++ if (m && !viaSiteAccount) {++ console.warn(`[PowerChat] ${ref} paid to a streamer account, not the site account — not a purchase`);++ return false;++ }if (m) {const order = db.getPaymentOrderById(Number(m[1]));if (!order || order.kind !== 'bucks') return true; // ours but unusable — never double-handle@@@ -333,6 +343,13 @@ function handleAttributedDonation(receivingUserId, data) {// discounted sub. Direct mode = the receiving account IS the streamer, so// falling through does the right thing; site mode routes explicitly.const mode = String(order.provider_ref || 'site').split(':')[0];++ // Direct: the tip must land on the subscribed streamer's own account (which may also be++ // the site tips account). Site-routed: it must land on the site tips account.++ const paidWhereExpected = mode === 'direct' ? (receivingUserId != null && Number(receivingUserId) === Number(order.streamer_id)) : viaSiteAccount;++ if (!paidWhereExpected) {++ console.warn(`[PowerChat] ${ref} (${mode}) paid to the wrong account — not a subscription`);++ return false;++ }const autoRenew = /:renew$/.test(String(order.provider_ref || '')) ? 1 : 0;const feeCents = Number((String(order.provider_ref || '').match(/:fee=(\d+)/) || [])[1] || 0);if (usdCents + 1 < (order.amount_cents || 0)) {@@@ -351,6 +368,10 @@ function handleAttributedDonation(receivingUserId, data) {}// Site-routed donation ----------------------------------------------m = ref.match(/^pcdon:(\d+):(\d+)$/);++ if (m && !viaSiteAccount) {++ console.warn(`[PowerChat] ${ref} paid to a streamer account, not the site account — not a site-routed donation`);++ return false;++ }if (m) {_creditSiteRoutedDonation(Number(m[1]), Number(m[2]) || null, data);return true;@@@ -52,12 +52,12 @@ function _hostFor(order) {const mode = String(order.provider_ref || 'site').split(':')[0];if (order.kind === 'subscription' && mode === 'direct' && order.streamer_id) {const conn = db.getPowerchatConnection(order.streamer_id);-- return conn && conn.powerchat_username ? { username: conn.powerchat_username, conn, receivingUserId: order.streamer_id } : null;++ return conn && conn.powerchat_username ? { username: conn.powerchat_username, conn, receivingUserId: order.streamer_id, viaSiteAccount: false } : null;}const site = checkout.getSiteAccount();if (!site) return null;const conn = db.getPowerchatConnectionByUsername(site.username);-- return { username: site.username, conn: conn || null, receivingUserId: conn ? conn.user_id : null };++ return { username: site.username, conn: conn || null, receivingUserId: conn ? conn.user_id : null, viaSiteAccount: true };}function _refFor(order) {return order.kind === 'subscription' ? `pcsub:${order.id}` : `pcorder:${order.id}`;@@@ -150,7 +150,7 @@ async function reconcileOnce() {appExternalRef: ref, isTest, reconciled: true,};let consumed = false;-- try { consumed = checkout.handleAttributedDonation(host.receivingUserId, data); }++ try { consumed = checkout.handleAttributedDonation(host.receivingUserId, data, { viaSiteAccount: host.viaSiteAccount }); }catch (e) { summary.errors++; console.warn(`[PowerChat] reconcile: fulfillment for ${ref} failed: ${e.message}`); }orders.delete(ref);let after = null;@@@ -232,7 +232,10 @@ function processEvent(envelope) {// checkout module; when it consumes the event, the receiving account's// normal donation pipeline must NOT also run.if (data.appExternalRef && /^(pcorder|pcsub|pcdon):/.test(String(data.appExternalRef))) {-- if (require('./powerchat-checkout').handleAttributedDonation(userId, data)) break;++ const siteName = String(db.getSetting('powerchat_site_tip_username') || '').trim().toLowerCase();++ const receiving = String((envelope.streamer && envelope.streamer.username) || '').toLowerCase();++ const viaSiteAccount = !!siteName && receiving === siteName;++ if (require('./powerchat-checkout').handleAttributedDonation(userId, data, { viaSiteAccount })) break;}_handleDonation(userId, data);break;nnew file mode 100644@@@ -0,0 +1,63 @@++'use strict';++// A PowerChat checkout reference (pcorder/pcsub/pcdon) rides in the tip link, so the buyer can edit it.++// It must only count when the money landed where that checkout sends it: the SITE tips account for++// purchases, site-routed donations and site-routed subscriptions; the subscribed streamer's own++// account for a direct subscription. A streamer tipping their own PowerChat with `pcdon:<self>`, or a++// viewer tipping their own account with `pcsub:<x>`, must fall through to ordinary tip handling.++const assert = require('assert');++const path = require('path');++function stub(modPath, exportsObj) { const full = require.resolve(modPath); require.cache[full] = { id: full, filename: full, loaded: true, exports: exportsObj }; }++++const orders = new Map();++const credited = { cashout: [], bucks: [], subs: [] };++const seed = () => {++ orders.clear();++ orders.set(41, { id: 41, kind: 'bucks', amount_cents: 500, bucks: 500, user_id: 7, status: 'pending' });++ orders.set(42, { id: 42, kind: 'subscription', amount_cents: 500, user_id: 8, streamer_id: 3, provider_ref: 'direct', status: 'pending' });++ orders.set(43, { id: 43, kind: 'subscription', amount_cents: 500, user_id: 9, streamer_id: 3, provider_ref: 'site', status: 'pending' });++ credited.cashout.length = 0; credited.bucks.length = 0; credited.subs.length = 0;++};++stub(path.join(__dirname, '../server/db/database'), {++ getSetting: (k) => (k === 'powerchat_site_tip_username' ? 'siteacct' : null),++ getPaymentOrderById: (id) => orders.get(id) || null,++ updatePaymentOrder: (id, f) => Object.assign(orders.get(id), f),++ getUserById: (id) => ({ id, username: 'u' + id }),++ addVibesCashout: (uid, cents) => credited.cashout.push([uid, cents]),++ createTransaction: () => {},++ getPowerchatConnectionByUsername: () => null,++ getPowerchatConnection: () => null,++});++stub(path.join(__dirname, '../server/monetization/payments'), {++ bucksForUsd: (usd) => Math.round(usd * 100),++ fulfillBucksOrder: (o) => { credited.bucks.push(o.id); orders.get(o.id).status = 'credited'; },++ fulfillSubscriptionOrder: (o) => { credited.subs.push(o.id); orders.get(o.id).status = 'credited'; },++});++stub(path.join(__dirname, '../server/integrations/powerchat-webhook'), { creditDonationPipeline: () => {} });++stub(path.join(__dirname, '../server/utils/notify'), { pushNotification: () => {} });++const checkout = require('../server/integrations/powerchat-checkout');++const log = console.warn; console.warn = () => {};++const tip = (ref, cents = 500) => ({ appExternalRef: ref, amountUsdCents: cents });++++seed();++// Exploit: streamer 3 tips their OWN PowerChat with a site-routed donation ref naming themselves.++assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcdon:3:0'), { viaSiteAccount: false }), false, 'pcdon on a streamer account is an ordinary tip');++assert.deepStrictEqual(credited.cashout, [], 'no cashout Vibes minted');++// Exploit: a purchase ref on a streamer account credits no Vibes.++assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcorder:41'), { viaSiteAccount: false }), false);++assert.deepStrictEqual(credited.bucks, []); assert.strictEqual(orders.get(41).status, 'pending');++// Exploit: viewer 9 tips their own account (user 9) with a direct sub ref for streamer 3.++assert.strictEqual(checkout.handleAttributedDonation(9, tip('pcsub:42'), { viaSiteAccount: false }), false, 'direct sub must be paid to the subscribed streamer');++// A site-routed sub ref paid to a streamer account is not a subscription either.++assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcsub:43'), { viaSiteAccount: false }), false);++assert.deepStrictEqual(credited.subs, []);++++// Legitimate paths still work.++assert.strictEqual(checkout.handleAttributedDonation(null, tip('pcorder:41')), true, 'purchase on the site account');++assert.deepStrictEqual(credited.bucks, [41]);++assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcsub:42'), { viaSiteAccount: false }), true, 'direct sub paid to streamer 3');++assert.strictEqual(checkout.handleAttributedDonation(1, tip('pcsub:43'), { viaSiteAccount: true }), true, 'site sub on the site account (even when the site account is a connected user)');++assert.deepStrictEqual(credited.subs, [42, 43]);++assert.strictEqual(checkout.handleAttributedDonation(null, tip('pcdon:3:7')), true, 'site-routed donation on the site account');++assert.deepStrictEqual(credited.cashout, [[3, 500]]);++console.warn = log;++console.log('✅ PowerChat checkout refs count only on the account the checkout routes to');
@@ -45,13 +45,21 @@ loadNetworkPublicKey();
@@ -45,13 +45,21 @@ loadNetworkPublicKey();
* Verify a openvibe.network RS256 JWT token.
* Verify a openvibe.network RS256 JWT token.
* Returns decoded payload or null.
* Returns decoded payload or null.
*/
*/
+// A user session token carries neither `typ` (FedCM assertions: 5 minutes, minted for one origin, to be
+// exchanged at Network, never a session) nor `actor_type` (service/app principal tokens). Both are
+// signed with the same Network key, so the signature alone does not make a token a login.
+function isUserSessionClaims(decoded) {
+ return !!decoded && typeof decoded === 'object' && decoded.typ === undefined && decoded.actor_type === undefined;
+}
+
function verifyToken(token) {
function verifyToken(token) {
if (!openvibeToolsPublicKey) return null;
if (!openvibeToolsPublicKey) return null;
try {
try {
- return jwt.verify(token, openvibeToolsPublicKey, {
+ const decoded = jwt.verify(token, openvibeToolsPublicKey, {
algorithms: ['RS256'],
algorithms: ['RS256'],
issuer: getNetworkIssuer(),
issuer: getNetworkIssuer(),
});
});
+ return isUserSessionClaims(decoded) ? decoded : null;
} catch {
} catch {
return null;
return null;
}
}
@@ -68,6 +76,7 @@ function verifyTokenWithReason(token) {
@@ -68,6 +76,7 @@ function verifyTokenWithReason(token) {
algorithms: ['RS256'],
algorithms: ['RS256'],
issuer: getNetworkIssuer(),
issuer: getNetworkIssuer(),
});
});
+ if (!isUserSessionClaims(decoded)) return { ok: false, reason: 'not_a_session_token' };
return { ok: true, decoded };
return { ok: true, decoded };
} catch (err) {
} catch (err) {
return { ok: false, reason: err.name, message: err.message };
return { ok: false, reason: err.name, message: err.message };
@@ -300,12 +300,22 @@ function buildDonateLink(streamerUserId, donorUserId, { goalId = null } = {}) {
@@ -300,12 +300,22 @@ function buildDonateLink(streamerUserId, donorUserId, { goalId = null } = {}) {
// matches one of our prefixes. Returns true when the event was CONSUMED (the normal
// matches one of our prefixes. Returns true when the event was CONSUMED (the normal
// donation pipeline for the receiving account must then be skipped), false to fall
// donation pipeline for the receiving account must then be skipped), false to fall
// through to plain donation handling.
// through to plain donation handling.
-function handleAttributedDonation(receivingUserId, data) {
+// The checkout reference rides in the tip link and the buyer can edit it, so a reference only
+// counts when the money landed where that kind of checkout sends it: purchases, site-routed
+// donations and site-routed subscriptions on the SITE tips account, a direct subscription on
+// the subscribed streamer's own account. Anything else (a streamer tipping their own PowerChat
+// with `pcdon:<self>`, a viewer tipping themselves with `pcsub:<x>`) is an ordinary tip to the
+// receiving account and falls through to normal handling.
+function handleAttributedDonation(receivingUserId, data, { viaSiteAccount = receivingUserId == null } = {}) {
const ref = String(data.appExternalRef || '');
const ref = String(data.appExternalRef || '');
const usdCents = Math.max(0, Math.round(Number(data.amountUsdCents || 0)));
const usdCents = Math.max(0, Math.round(Number(data.amountUsdCents || 0)));
try {
try {
// Buy Vibes ---------------------------------------------------------
// Buy Vibes ---------------------------------------------------------
let m = ref.match(/^pcorder:(\d+)$/);
let m = ref.match(/^pcorder:(\d+)$/);
+ if (m && !viaSiteAccount) {
+ console.warn(`[PowerChat] ${ref} paid to a streamer account, not the site account — not a purchase`);
+ return false;
+ }
if (m) {
if (m) {
const order = db.getPaymentOrderById(Number(m[1]));
const order = db.getPaymentOrderById(Number(m[1]));
if (!order || order.kind !== 'bucks') return true; // ours but unusable — never double-handle
if (!order || order.kind !== 'bucks') return true; // ours but unusable — never double-handle
@@ -333,6 +343,13 @@ function handleAttributedDonation(receivingUserId, data) {
@@ -333,6 +343,13 @@ function handleAttributedDonation(receivingUserId, data) {
// discounted sub. Direct mode = the receiving account IS the streamer, so
// discounted sub. Direct mode = the receiving account IS the streamer, so
// falling through does the right thing; site mode routes explicitly.
// falling through does the right thing; site mode routes explicitly.
const mode = String(order.provider_ref || 'site').split(':')[0];
const mode = String(order.provider_ref || 'site').split(':')[0];
+ // Direct: the tip must land on the subscribed streamer's own account (which may also be
+ // the site tips account). Site-routed: it must land on the site tips account.
+ const paidWhereExpected = mode === 'direct' ? (receivingUserId != null && Number(receivingUserId) === Number(order.streamer_id)) : viaSiteAccount;
+ if (!paidWhereExpected) {
+ console.warn(`[PowerChat] ${ref} (${mode}) paid to the wrong account — not a subscription`);
+ return false;
+ }
const autoRenew = /:renew$/.test(String(order.provider_ref || '')) ? 1 : 0;
const autoRenew = /:renew$/.test(String(order.provider_ref || '')) ? 1 : 0;
const feeCents = Number((String(order.provider_ref || '').match(/:fee=(\d+)/) || [])[1] || 0);
const feeCents = Number((String(order.provider_ref || '').match(/:fee=(\d+)/) || [])[1] || 0);
if (usdCents + 1 < (order.amount_cents || 0)) {
if (usdCents + 1 < (order.amount_cents || 0)) {
@@ -351,6 +368,10 @@ function handleAttributedDonation(receivingUserId, data) {
@@ -351,6 +368,10 @@ function handleAttributedDonation(receivingUserId, data) {
}
}
// Site-routed donation ----------------------------------------------
// Site-routed donation ----------------------------------------------
m = ref.match(/^pcdon:(\d+):(\d+)$/);
m = ref.match(/^pcdon:(\d+):(\d+)$/);
+ if (m && !viaSiteAccount) {
+ console.warn(`[PowerChat] ${ref} paid to a streamer account, not the site account — not a site-routed donation`);
+ return false;
+ }
if (m) {
if (m) {
_creditSiteRoutedDonation(Number(m[1]), Number(m[2]) || null, data);
_creditSiteRoutedDonation(Number(m[1]), Number(m[2]) || null, data);
return true;
return true;
@@ -52,12 +52,12 @@ function _hostFor(order) {
@@ -52,12 +52,12 @@ function _hostFor(order) {
const mode = String(order.provider_ref || 'site').split(':')[0];
const mode = String(order.provider_ref || 'site').split(':')[0];
if (order.kind === 'subscription' && mode === 'direct' && order.streamer_id) {
if (order.kind === 'subscription' && mode === 'direct' && order.streamer_id) {
const conn = db.getPowerchatConnection(order.streamer_id);
const conn = db.getPowerchatConnection(order.streamer_id);
- return conn && conn.powerchat_username ? { username: conn.powerchat_username, conn, receivingUserId: order.streamer_id } : null;
+ return conn && conn.powerchat_username ? { username: conn.powerchat_username, conn, receivingUserId: order.streamer_id, viaSiteAccount: false } : null;
}
}
const site = checkout.getSiteAccount();
const site = checkout.getSiteAccount();
if (!site) return null;
if (!site) return null;
const conn = db.getPowerchatConnectionByUsername(site.username);
const conn = db.getPowerchatConnectionByUsername(site.username);
- return { username: site.username, conn: conn || null, receivingUserId: conn ? conn.user_id : null };
+ return { username: site.username, conn: conn || null, receivingUserId: conn ? conn.user_id : null, viaSiteAccount: true };
}
}
function _refFor(order) {
function _refFor(order) {
return order.kind === 'subscription' ? `pcsub:${order.id}` : `pcorder:${order.id}`;
return order.kind === 'subscription' ? `pcsub:${order.id}` : `pcorder:${order.id}`;
@@ -150,7 +150,7 @@ async function reconcileOnce() {
@@ -150,7 +150,7 @@ async function reconcileOnce() {
appExternalRef: ref, isTest, reconciled: true,
appExternalRef: ref, isTest, reconciled: true,
};
};
let consumed = false;
let consumed = false;
- try { consumed = checkout.handleAttributedDonation(host.receivingUserId, data); }
+ try { consumed = checkout.handleAttributedDonation(host.receivingUserId, data, { viaSiteAccount: host.viaSiteAccount }); }
catch (e) { summary.errors++; console.warn(`[PowerChat] reconcile: fulfillment for ${ref} failed: ${e.message}`); }
catch (e) { summary.errors++; console.warn(`[PowerChat] reconcile: fulfillment for ${ref} failed: ${e.message}`); }
orders.delete(ref);
orders.delete(ref);
let after = null;
let after = null;
@@ -232,7 +232,10 @@ function processEvent(envelope) {
@@ -232,7 +232,10 @@ function processEvent(envelope) {
// checkout module; when it consumes the event, the receiving account's
// checkout module; when it consumes the event, the receiving account's
// normal donation pipeline must NOT also run.
// normal donation pipeline must NOT also run.
if (data.appExternalRef && /^(pcorder|pcsub|pcdon):/.test(String(data.appExternalRef))) {
if (data.appExternalRef && /^(pcorder|pcsub|pcdon):/.test(String(data.appExternalRef))) {
- if (require('./powerchat-checkout').handleAttributedDonation(userId, data)) break;
+ const siteName = String(db.getSetting('powerchat_site_tip_username') || '').trim().toLowerCase();
+ const receiving = String((envelope.streamer && envelope.streamer.username) || '').toLowerCase();
+ const viaSiteAccount = !!siteName && receiving === siteName;
+ if (require('./powerchat-checkout').handleAttributedDonation(userId, data, { viaSiteAccount })) break;
}
}
_handleDonation(userId, data);
_handleDonation(userId, data);
break;
break;
new file mode 100644
new file mode 100644
@@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
+'use strict';
+// A PowerChat checkout reference (pcorder/pcsub/pcdon) rides in the tip link, so the buyer can edit it.
+// It must only count when the money landed where that checkout sends it: the SITE tips account for
+// purchases, site-routed donations and site-routed subscriptions; the subscribed streamer's own
+// account for a direct subscription. A streamer tipping their own PowerChat with `pcdon:<self>`, or a
+// viewer tipping their own account with `pcsub:<x>`, must fall through to ordinary tip handling.
+const assert = require('assert');
+const path = require('path');
+function stub(modPath, exportsObj) { const full = require.resolve(modPath); require.cache[full] = { id: full, filename: full, loaded: true, exports: exportsObj }; }
+
+const orders = new Map();
+const credited = { cashout: [], bucks: [], subs: [] };
+const seed = () => {
+ orders.clear();
+ orders.set(41, { id: 41, kind: 'bucks', amount_cents: 500, bucks: 500, user_id: 7, status: 'pending' });
+ orders.set(42, { id: 42, kind: 'subscription', amount_cents: 500, user_id: 8, streamer_id: 3, provider_ref: 'direct', status: 'pending' });
+ orders.set(43, { id: 43, kind: 'subscription', amount_cents: 500, user_id: 9, streamer_id: 3, provider_ref: 'site', status: 'pending' });
+ credited.cashout.length = 0; credited.bucks.length = 0; credited.subs.length = 0;
+};
+stub(path.join(__dirname, '../server/db/database'), {
+ getSetting: (k) => (k === 'powerchat_site_tip_username' ? 'siteacct' : null),
+ getPaymentOrderById: (id) => orders.get(id) || null,
+ updatePaymentOrder: (id, f) => Object.assign(orders.get(id), f),
+ getUserById: (id) => ({ id, username: 'u' + id }),
+ addVibesCashout: (uid, cents) => credited.cashout.push([uid, cents]),
+ createTransaction: () => {},
+ getPowerchatConnectionByUsername: () => null,
+ getPowerchatConnection: () => null,
+});
+stub(path.join(__dirname, '../server/monetization/payments'), {
+ bucksForUsd: (usd) => Math.round(usd * 100),
+ fulfillBucksOrder: (o) => { credited.bucks.push(o.id); orders.get(o.id).status = 'credited'; },
+ fulfillSubscriptionOrder: (o) => { credited.subs.push(o.id); orders.get(o.id).status = 'credited'; },
+});
+stub(path.join(__dirname, '../server/integrations/powerchat-webhook'), { creditDonationPipeline: () => {} });
+stub(path.join(__dirname, '../server/utils/notify'), { pushNotification: () => {} });
+const checkout = require('../server/integrations/powerchat-checkout');
+const log = console.warn; console.warn = () => {};
+const tip = (ref, cents = 500) => ({ appExternalRef: ref, amountUsdCents: cents });
+
+seed();
+// Exploit: streamer 3 tips their OWN PowerChat with a site-routed donation ref naming themselves.
+assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcdon:3:0'), { viaSiteAccount: false }), false, 'pcdon on a streamer account is an ordinary tip');
+assert.deepStrictEqual(credited.cashout, [], 'no cashout Vibes minted');
+// Exploit: a purchase ref on a streamer account credits no Vibes.
+assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcorder:41'), { viaSiteAccount: false }), false);
+assert.deepStrictEqual(credited.bucks, []); assert.strictEqual(orders.get(41).status, 'pending');
+// Exploit: viewer 9 tips their own account (user 9) with a direct sub ref for streamer 3.
+assert.strictEqual(checkout.handleAttributedDonation(9, tip('pcsub:42'), { viaSiteAccount: false }), false, 'direct sub must be paid to the subscribed streamer');
+// A site-routed sub ref paid to a streamer account is not a subscription either.
+assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcsub:43'), { viaSiteAccount: false }), false);
+assert.deepStrictEqual(credited.subs, []);
+
+// Legitimate paths still work.
+assert.strictEqual(checkout.handleAttributedDonation(null, tip('pcorder:41')), true, 'purchase on the site account');
+assert.deepStrictEqual(credited.bucks, [41]);
+assert.strictEqual(checkout.handleAttributedDonation(3, tip('pcsub:42'), { viaSiteAccount: false }), true, 'direct sub paid to streamer 3');
+assert.strictEqual(checkout.handleAttributedDonation(1, tip('pcsub:43'), { viaSiteAccount: true }), true, 'site sub on the site account (even when the site account is a connected user)');
+assert.deepStrictEqual(credited.subs, [42, 43]);
+assert.strictEqual(checkout.handleAttributedDonation(null, tip('pcdon:3:7')), true, 'site-routed donation on the site account');
+assert.deepStrictEqual(credited.cashout, [[3, 500]]);
+console.warn = log;
+console.log('✅ PowerChat checkout refs count only on the account the checkout routes to');