Files
mortgagefi-helper/app/api/cron/route.ts
Siavash Sameni 6ae581ab2e feat(ui): Ghibli/Miyazaki reskin + Obsidian docs vault + project audit
UI: warm daylight design system (Tailwind v4 @theme palette, gh-* component
classes, watercolor grain, Zen Maru Gothic + Klee One fonts), animated SSR-safe
GhibliBackground (drifting clouds, meadow hills, soot sprites), and a full reskin
of navbar, connect button, dapp page, loan cards, settings modal, and readme.
Fixes the bg-white-on-dark loan-card inconsistency. Web3/business logic untouched.

Docs: converted docs/ into an Obsidian vault (frontmatter, [[wikilinks]],
callouts, Home MOC, folders Architecture/Operations/Audits) and added a
full-project audit note (Project Audit 2026-06). Redacted a real leaked Schedy
key value from the security audit example (rotate it at Schedy).

Also commits the previously-untracked server layer: app/api (cron + tasks routes)
and lib (redis, ssrf-guard, task-store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 08:13:53 +04:00

84 lines
2.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { listDueTasks, deleteTask } from '@/lib/task-store';
import { sanitizeHeaders } from '@/lib/ssrf-guard';
const CRON_SECRET = process.env.CRON_SECRET;
export async function GET(request: NextRequest) {
// Protect cron endpoint: require Authorization header with CRON_SECRET
const auth = request.headers.get('authorization') || '';
const expected = CRON_SECRET ? `Bearer ${CRON_SECRET}` : '';
if (!CRON_SECRET || auth !== expected) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const nowSec = Math.floor(Date.now() / 1000);
let tasks: Awaited<ReturnType<typeof listDueTasks>>;
try {
tasks = await listDueTasks(nowSec);
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'Failed to fetch tasks' }, { status: 500 });
}
const results: Array<{ id: string; ok: boolean; error?: string }> = [];
for (const task of tasks) {
let ok = false;
let lastError: string | undefined;
for (let attempt = 0; attempt <= task.retries; attempt++) {
if (attempt > 0) {
await sleep(task.retryInterval);
}
try {
const bodyBytes = encodePayload(task.payload);
const res = await fetch(task.url, {
method: 'POST',
headers: {
...sanitizeHeaders(task.headers),
'content-type': guessContentType(task.payload),
},
body: bodyBytes,
signal: AbortSignal.timeout(15000),
});
if (res.ok) {
ok = true;
break;
}
lastError = `HTTP ${res.status}`;
} catch (e: any) {
lastError = e?.message || 'Network error';
}
}
if (ok) {
try {
await deleteTask(task.id);
} catch {
// best-effort cleanup
}
}
results.push({ id: task.id, ok, error: ok ? undefined : lastError });
}
return NextResponse.json({ executed: results.length, results });
}
function encodePayload(payload: any): string {
if (payload === null || payload === undefined) return '';
if (typeof payload === 'string') return payload;
return JSON.stringify(payload);
}
function guessContentType(payload: any): string {
if (typeof payload === 'string') return 'text/plain';
return 'application/json';
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}