import fs from "node:fs";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const HEADERS = {
Authorization: `Bearer ${process.env.MODA_API_KEY!}`,
"Moda-Version": "2026-05-01",
};
const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.EXPORT_BUCKET!;
async function* listAllCanvases() {
let cursor: string | null = null;
for (;;) {
const u = new URL("https://api.moda.app/v1/canvases");
u.searchParams.set("limit", "100");
if (cursor) u.searchParams.set("cursor", cursor);
const { data, next_cursor } = await fetch(u, { headers: HEADERS }).then(r => r.json());
for (const c of data) yield c;
if (!next_cursor) return;
cursor = next_cursor;
}
}
async function exportWithRetry(canvasId: string, retries = 3): Promise<string | null> {
attempts: for (let attempt = 1; attempt <= retries; attempt++) {
const res = await fetch(
`https://api.moda.app/v1/canvases/${canvasId}/export?format=pdf`,
{ method: "POST", headers: HEADERS },
);
if (res.ok) {
let exp = await res.json();
if (exp.status === "in_progress") { // slow render; poll it out
const taskId = exp.task_id;
for (;;) {
await new Promise(r => setTimeout(r, (exp.retry_after_seconds ?? 5) * 1000));
const poll = await fetch(
`https://api.moda.app/v1/canvases/${canvasId}/export-status?task_id=${taskId}`,
{ headers: HEADERS },
);
if (!poll.ok) continue attempts; // 429/5xx polling — not an export failure
exp = await poll.json();
if (exp.is_terminal !== false) break; // terminal, or a shape we don't recognise
}
}
if (exp.status !== "completed") {
console.error(`Canvas ${canvasId}: export failed`, exp.error, exp.error_code);
if (exp.retryable) continue; // transient — spend another attempt
return null; // terminal for this canvas's content
}
return exp.url;
}
if (res.status === 409) { // canvas_active_job
const wait = Number(res.headers.get("Retry-After") ?? 10) * 1000;
console.log(`Canvas ${canvasId}: task running; waiting ${wait / 1000}s`);
await new Promise(r => setTimeout(r, wait));
continue;
}
if (res.status === 429) { // rate limit
const wait = Number(res.headers.get("Retry-After") ?? 10) * 1000;
await new Promise(r => setTimeout(r, wait));
continue;
}
const body = await res.json().catch(() => null);
console.error(`Canvas ${canvasId}: export failed`, body?.error);
return null;
}
return null;
}
for await (const canvas of listAllCanvases()) {
const exportUrl = await exportWithRetry(canvas.id);
if (!exportUrl) continue;
const bytes = Buffer.from(await (await fetch(exportUrl)).arrayBuffer());
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: `canvases/${canvas.id}.pdf`,
Body: bytes,
ContentType: "application/pdf",
Metadata: { "canvas-name": canvas.name, "updated-at": canvas.updated_at },
}));
console.log(`Archived ${canvas.name} (${canvas.id})`);
}