kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
import { Router } from "express";
import { db, configTable, geminiAccountsTable } from "@workspace/db";
import { eq, asc, isNull, and, notInArray } from "drizzle-orm";
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
import { getTurnstileToken, invalidateTurnstileToken } from "../captcha.js";
import { generateGuardId } from "../guardId.js";
const router = Router();
const TOKEN_KEY = "geminigen_bearer_token";
const REFRESH_TOKEN_KEY = "geminigen_refresh_token";
const USERNAME_KEY = "geminigen_username";
const PASSWORD_KEY = "geminigen_password_enc";
const GEMINIGEN_BASE = "https://api.geminigen.ai/api";
const GEMINIGEN_REFRESH_URL = `${GEMINIGEN_BASE}/refresh-token`;
const GEMINIGEN_SIGNIN_URL = `${GEMINIGEN_BASE}/login-v2`;
const UA = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.6367.111 Mobile/15E148 Safari/604.1";
// ── Encryption helpers (AES-256-GCM) ─────────────────────────────────────────
function getEncKey(): Buffer {
const secret = process.env.SESSION_SECRET || "starforge-default-secret-change-me";
return scryptSync(secret, "starforge-salt", 32);
}
export function encrypt(text: string): string {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", getEncKey(), iv);
const enc = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return iv.toString("hex") + ":" + tag.toString("hex") + ":" + enc.toString("hex");
}
export function decrypt(encoded: string): string {
const [ivHex, tagHex, encHex] = encoded.split(":");
const decipher = createDecipheriv("aes-256-gcm", getEncKey(), Buffer.from(ivHex, "hex"));
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
return decipher.update(Buffer.from(encHex, "hex")).toString("utf8") + decipher.final("utf8");
}
// ── DB helpers ────────────────────────────────────────────────────────────────
async function getConfig(key: string): Promise<string | null> {
const row = await db.select().from(configTable).where(eq(configTable.key, key)).limit(1);
return row.length > 0 ? row[0].value : null;
}
async function setConfig(key: string, value: string): Promise<void> {
await db
.insert(configTable)
.values({ key, value, updatedAt: new Date() })
.onConflictDoUpdate({ target: configTable.key, set: { value, updatedAt: new Date() } });
}
// ── Public helpers ────────────────────────────────────────────────────────────
export async function getStoredToken(): Promise<string | null> {
return getConfig(TOKEN_KEY);
}
export async function getStoredRefreshToken(): Promise<string | null> {
return getConfig(REFRESH_TOKEN_KEY);
}
/**
* On-demand token getter that proactively refreshes if the stored token is
* older than 50 minutes. Safe to call in serverless environments where there
* is no background refresh loop running.
*/
export async function getValidBearerToken(): Promise<string | null> {
const rows = await db
.select()
.from(configTable)
.where(eq(configTable.key, TOKEN_KEY))
.limit(1);
if (!rows.length) return null;
const row = rows[0];
const ageMs = Date.now() - new Date(row.updatedAt).getTime();
const fiftyMinutes = 50 * 60 * 1000;
if (ageMs > fiftyMinutes) {
console.log("[token] Token age > 50 min, proactively refreshing...");
const fresh = await refreshAccessToken();
if (fresh) return fresh;
}
return row.value;
}
export async function getStoredCredentials(): Promise<{ username: string; password: string } | null> {
const [username, passwordEnc] = await Promise.all([getConfig(USERNAME_KEY), getConfig(PASSWORD_KEY)]);
if (!username || !passwordEnc) return null;
try {
return { username, password: decrypt(passwordEnc) };
} catch {
return null;
}
}
// ── Login with email/password β†’ get fresh access+refresh tokens ───────────────
async function loginWithCredentials(): Promise<{ accessToken: string; refreshToken: string } | null> {
const creds = await getStoredCredentials();
if (!creds) return null;
try {
console.log("[token-refresh] Trying credential re-login via /login-v2...");
// /api/login-v2 requires: username, password, turnstile_token (JSON body)
const turnstileToken = await getTurnstileToken();
const resp = await fetch(GEMINIGEN_SIGNIN_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA,
"x-guard-id": generateGuardId("/api/login-v2", "post"),
},
body: JSON.stringify({ username: creds.username, password: creds.password, turnstile_token: turnstileToken }),
});
const text = await resp.text();
console.log(`[token-refresh] signin β†’ HTTP ${resp.status}: ${text.slice(0, 200)}`);
if (!resp.ok) {
invalidateTurnstileToken(); // force fresh token next attempt
return null;
}
const data = JSON.parse(text) as { access_token?: string; refresh_token?: string };
if (!data.access_token) return null;
return { accessToken: data.access_token, refreshToken: data.refresh_token || "" };
} catch (e: any) {
console.log(`[token-refresh] signin threw: ${e?.message}`);
return null;
}
}
// ── Core: refresh access token (with auto-login fallback) ─────────────────────
export async function refreshAccessToken(): Promise<string | null> {
const refreshToken = await getStoredRefreshToken();
// β‘  Try native refresh endpoint if we have a refresh_token
if (refreshToken) {
try {
console.log("[token-refresh] Trying refresh_token endpoint...");
const resp = await fetch(GEMINIGEN_REFRESH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA,
"x-guard-id": generateGuardId("/api/refresh-token", "post"),
},
body: JSON.stringify({ refresh_token: refreshToken }),
});
const text = await resp.text();
console.log(`[token-refresh] refresh β†’ HTTP ${resp.status}: ${text.slice(0, 200)}`);
if (resp.ok) {
const data = JSON.parse(text) as { access_token?: string; refresh_token?: string };
if (data.access_token) {
await setConfig(TOKEN_KEY, data.access_token);
if (data.refresh_token) await setConfig(REFRESH_TOKEN_KEY, data.refresh_token);
console.log("[token-refresh] Success via refresh_token");
return data.access_token;
}
}
const isExpired = text.includes("REFRESH_TOKEN_EXPIRED") || text.includes("expired");
if (!isExpired) {
console.warn("[token-refresh] Refresh failed for unknown reason β€” won't try credentials");
return null;
}
console.log("[token-refresh] Refresh token expired β€” falling back to credential login");
} catch (e: any) {
console.log(`[token-refresh] refresh threw: ${e?.message}`);
}
} else {
console.log("[token-refresh] No refresh token stored β€” trying credential login directly");
}
// β‘‘ Fallback: login with stored email/password
const result = await loginWithCredentials();
if (!result) {
console.warn("[token-refresh] All strategies failed β€” no credentials stored or login failed");
return null;
}
await setConfig(TOKEN_KEY, result.accessToken);
if (result.refreshToken) await setConfig(REFRESH_TOKEN_KEY, result.refreshToken);
console.log("[token-refresh] Success via credential re-login");
return result.accessToken;
}
// ── Token Pool helpers ────────────────────────────────────────────────────────
const GEMINIGEN_REFRESH = "https://api.geminigen.ai/api/refresh-token";
/** Get the least-recently-used active account from the pool. Returns null if pool is empty. */
export async function getPoolToken(excludeIds: number[] = []): Promise<{ token: string; accountId: number } | null> {
const query = db
.select()
.from(geminiAccountsTable)
.where(
excludeIds.length > 0
? and(eq(geminiAccountsTable.isActive, true), notInArray(geminiAccountsTable.id, excludeIds))
: eq(geminiAccountsTable.isActive, true)
)
.orderBy(asc(geminiAccountsTable.lastUsedAt))
.limit(1);
const rows = await query;
if (!rows.length) return null;
const account = rows[0];
return { token: account.bearerToken, accountId: account.id };
}
/** Mark an account as just used (update lastUsedAt). */
export async function markAccountUsed(accountId: number): Promise<void> {
await db
.update(geminiAccountsTable)
.set({ lastUsedAt: new Date() })
.where(eq(geminiAccountsTable.id, accountId));
}
/** Try to refresh a specific pool account's token via its refresh_token. Returns new token or null. */
export async function tryRefreshPoolAccount(accountId: number): Promise<string | null> {
const rows = await db.select().from(geminiAccountsTable).where(eq(geminiAccountsTable.id, accountId)).limit(1);
if (!rows.length) return null;
const account = rows[0];
if (!account.refreshToken) return null;
try {
const resp = await fetch(GEMINIGEN_REFRESH, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA,
"x-guard-id": generateGuardId("/api/refresh-token", "post"),
},
body: JSON.stringify({ refresh_token: account.refreshToken }),
});
if (!resp.ok) return null;
const data = await resp.json() as { access_token?: string; refresh_token?: string };
if (!data.access_token) return null;
await db.update(geminiAccountsTable).set({
bearerToken: data.access_token,
...(data.refresh_token ? { refreshToken: data.refresh_token } : {}),
}).where(eq(geminiAccountsTable.id, accountId));
console.log(`[pool] Account ${accountId} token refreshed`);
return data.access_token;
} catch {
return null;
}
}
/** Disable an account in the pool (e.g., when all refresh strategies fail). */
export async function disablePoolAccount(accountId: number): Promise<void> {
await db.update(geminiAccountsTable).set({ isActive: false }).where(eq(geminiAccountsTable.id, accountId));
console.warn(`[pool] Account ${accountId} disabled β€” token unrecoverable`);
}
// ── Routes ────────────────────────────────────────────────────────────────────
router.get("/token", async (_req, res) => {
const token = await getConfig(TOKEN_KEY);
if (!token) return res.json({ configured: false, token: null });
return res.json({ configured: true, token: token.substring(0, 10) + "..." });
});
router.post("/token", async (req, res) => {
const { token, refreshToken } = req.body as { token?: string; refreshToken?: string };
if (!token?.trim()) return res.status(400).json({ error: "INVALID_TOKEN", message: "Token is required" });
await setConfig(TOKEN_KEY, token.trim());
if (refreshToken?.trim()) await setConfig(REFRESH_TOKEN_KEY, refreshToken.trim());
res.json({ success: true, message: "Token saved successfully" });
});
router.delete("/token", async (_req, res) => {
await db.delete(configTable).where(eq(configTable.key, TOKEN_KEY));
await db.delete(configTable).where(eq(configTable.key, REFRESH_TOKEN_KEY));
res.json({ success: true, message: "Token removed" });
});
export default router;