Initial commit — OFApp client + server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-12 20:07:06 -06:00
commit c60de19348
43 changed files with 8679 additions and 0 deletions

84
server/signing.js Normal file
View File

@@ -0,0 +1,84 @@
import { createHash } from 'crypto';
import fetch from 'node-fetch';
// Try multiple community-maintained rule sources in order
const RULES_URLS = [
'https://raw.githubusercontent.com/rafa-9/dynamic-rules/main/rules.json',
'https://raw.githubusercontent.com/datawhores/onlyfans-dynamic-rules/main/dynamicRules.json',
'https://raw.githubusercontent.com/DATAHOARDERS/dynamic-rules/main/onlyfans.json',
];
const REFRESH_INTERVAL = 60 * 60 * 1000; // 1 hour
let rules = null;
let lastFetch = 0;
function normalizeRules(raw) {
// Different sources use different key names — normalize them
return {
static_param: raw.static_param,
checksum_indexes: raw.checksum_indexes,
checksum_constant: raw.checksum_constant ?? 0,
checksum_constants: raw.checksum_constants ?? null, // per-index constants (some sources)
app_token: raw.app_token || raw['app-token'] || '33d57ade8c02dbc5a333db99ff9ae26a',
prefix: raw.prefix || raw.format?.split(':')[0],
suffix: raw.suffix || raw.format?.split(':').pop(),
remove_headers: raw.remove_headers ?? [],
};
}
async function fetchRules() {
for (const url of RULES_URLS) {
try {
const res = await fetch(url);
if (!res.ok) continue;
const raw = await res.json();
rules = normalizeRules(raw);
lastFetch = Date.now();
console.log(`[signing] Rules loaded from ${url} (prefix: ${rules.prefix})`);
return rules;
} catch (err) {
console.warn(`[signing] Failed to fetch from ${url}: ${err.message}`);
}
}
throw new Error('All dynamic rules sources failed');
}
export async function initRules() {
await fetchRules();
}
export function getRules() {
if (Date.now() - lastFetch > REFRESH_INTERVAL) {
fetchRules().catch((err) => console.error('[signing] Failed to refresh rules:', err.message));
}
return rules;
}
export function createSignedHeaders(path, userId) {
if (!rules) throw new Error('Signing rules not initialized');
const timestamp = Date.now().toString();
// Use "0" for userId when user-id is in remove_headers
const signUserId = rules.remove_headers?.includes('user-id') ? '0' : userId;
const message = [rules.static_param, timestamp, path, signUserId].join('\n');
const sha1Hex = createHash('sha1').update(message).digest('hex');
const hexBytes = Buffer.from(sha1Hex, 'ascii');
let checksum = 0;
for (let i = 0; i < rules.checksum_indexes.length; i++) {
const byteVal = hexBytes[rules.checksum_indexes[i]];
const perIndex = rules.checksum_constants?.[i] ?? 0;
checksum += byteVal + perIndex;
}
checksum += rules.checksum_constant;
const sign = `${rules.prefix}:${sha1Hex}:${Math.abs(checksum).toString(16)}:${rules.suffix}`;
return {
sign,
time: timestamp,
'app-token': rules.app_token,
};
}