Spaces:
Running
Running
File size: 3,735 Bytes
dcf8b6b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | import {
Keypair,
TransactionBuilder,
Operation,
Asset,
Horizon,
Memo,
} from "@stellar/stellar-sdk";
import { NETWORK_PASSPHRASE, HORIZON_URL, USDC_ISSUER } from "./network-config.js";
const USDC = new Asset("USDC", USDC_ISSUER);
// Per-payer refund rate limit. Throws off attrition attacks (paying $0.001
// repeatedly to drain XLM via tx fees) without blocking legitimate users.
// In-memory only — fine for single-instance HF Space; revisit if we scale.
const REFUND_WINDOW_MS = 60 * 60 * 1000; // 1 hour
const REFUND_MAX_PER_WINDOW = 10;
const refundLog = new Map(); // payerAddress -> [timestamp, ...]
function isRefundRateLimited(payerAddress) {
const now = Date.now();
const cutoff = now - REFUND_WINDOW_MS;
const history = (refundLog.get(payerAddress) || []).filter((t) => t > cutoff);
if (history.length >= REFUND_MAX_PER_WINDOW) {
refundLog.set(payerAddress, history);
return true;
}
history.push(now);
refundLog.set(payerAddress, history);
return false;
}
// Periodic prune so the map doesn't grow unbounded.
setInterval(() => {
const cutoff = Date.now() - REFUND_WINDOW_MS;
for (const [addr, history] of refundLog) {
const kept = history.filter((t) => t > cutoff);
if (kept.length === 0) refundLog.delete(addr);
else refundLog.set(addr, kept);
}
}, REFUND_WINDOW_MS).unref();
const BLOCKED_PATTERNS = [
"you've been blocked",
"you have been blocked",
"blocked by network security",
"access denied",
"please log in",
"log in to",
"sign in to",
"enable javascript",
"just a moment...",
"checking your browser",
"performing security verification",
"ray id:",
"cloudflare",
"attention required",
"please verify you are a human",
"this page doesn't exist",
];
export function isFailedRender(content, title) {
if (!content || content.length < 100) return "empty_content";
const lower = content.toLowerCase();
const titleLower = (title || "").toLowerCase();
// Check title for block signals
if (titleLower === "just a moment..." || titleLower === "blocked") {
return "blocked_page";
}
// Check content for block/login patterns
for (const pattern of BLOCKED_PATTERNS) {
// Only flag if the blocked pattern is a large portion of the content
// (avoid false positives on pages that mention "log in" in a nav bar)
if (lower.includes(pattern) && content.length < 500) {
return "blocked_page";
}
}
return null; // render is good
}
export async function sendRefund(payerAddress, amount, memo) {
const serverSecret = process.env.SERVER_SECRET;
if (!serverSecret) {
console.error("SERVER_SECRET not set — cannot send refund");
return null;
}
if (isRefundRateLimited(payerAddress)) {
console.warn(
`Refund rate-limited for ${payerAddress} (>${REFUND_MAX_PER_WINDOW}/hr) — skipping`,
);
return null;
}
try {
const server = new Horizon.Server(HORIZON_URL);
const keypair = Keypair.fromSecret(serverSecret);
const account = await server.loadAccount(keypair.publicKey());
const tx = new TransactionBuilder(account, {
fee: "100",
networkPassphrase: NETWORK_PASSPHRASE,
})
.addOperation(
Operation.payment({
destination: payerAddress,
asset: USDC,
amount: amount,
}),
)
.addMemo(new Memo("text", (memo || "RenderGate refund").slice(0, 28)))
.setTimeout(30)
.build();
tx.sign(keypair);
const result = await server.submitTransaction(tx);
console.log(`Refund sent to ${payerAddress}: ${result.hash}`);
return result.hash;
} catch (err) {
console.error(`Refund failed for ${payerAddress}:`, err.message);
return null;
}
}
|