-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.mjs
More file actions
124 lines (101 loc) · 3.55 KB
/
Copy pathserver.mjs
File metadata and controls
124 lines (101 loc) · 3.55 KB
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
import express from "express";
const app = express();
const port = Number(process.env.PORT || 3000);
const reconcileTimeoutMs = Math.max(
1000,
Number(process.env.APIDOT_RECONCILE_TIMEOUT_MS || 5000) || 5000,
);
app.use(express.json({ limit: "2mb" }));
const lastStatusByTaskId = new Map();
const submittedTaskIds = new Set(
(process.env.APIDOT_KNOWN_TASK_IDS || "")
.split(",")
.map((taskId) => taskId.trim())
.filter(Boolean),
);
const allowUnlistedTaskIds = process.env.APIDOT_ALLOW_UNLISTED_TASK_IDS === "true";
async function isKnownTaskId(taskId) {
// Use APIDOT_ALLOW_UNLISTED_TASK_IDS=true only for local demos.
// In production, replace this with a database lookup.
return submittedTaskIds.has(taskId) || allowUnlistedTaskIds;
}
function createTimeoutSignal(timeoutMs) {
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
return AbortSignal.timeout(timeoutMs);
}
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs);
return controller.signal;
}
async function reconcileTaskStatus(taskId, { timeoutMs = 5000 } = {}) {
const apiKey = process.env.APIDOT_API_KEY;
const baseUrl = process.env.APIDOT_BASE_URL || "https://api.apidot.ai";
if (!apiKey || apiKey === "YOUR_API_KEY_HERE") {
return { task_id: taskId, reconciled: false, reason: "APIDOT_API_KEY is not set" };
}
try {
const response = await fetch(`${baseUrl}/api/generate/status/${taskId}`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
signal: createTimeoutSignal(timeoutMs),
});
const body = await response.json().catch(() => ({}));
return {
task_id: taskId,
reconciled: response.ok,
http_status: response.status,
status: body?.data?.status,
};
} catch (error) {
const name = error?.name || "Error";
const message = error?.message || "Unknown status request error";
return {
task_id: taskId,
reconciled: false,
reason: name === "AbortError" || name === "TimeoutError" ? "status request timed out" : message,
};
}
}
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
app.post("/api/apidot/webhook", async (req, res) => {
const event = req.body;
const taskId = event?.data?.task_id || event?.task_id;
if (!taskId) {
return res.status(400).json({ ok: false, error: "Missing task_id" });
}
if (!(await isKnownTaskId(taskId))) {
return res.status(202).json({ ok: true, ignored: true, reason: "Unknown task_id" });
}
const status = event?.data?.status || event?.status || "unknown";
const files = event?.data?.files || event?.files || [];
// Demo-only duplicate suppression. Production should use durable idempotency
// based on task_id plus a status version, update time, or business unique key.
if (lastStatusByTaskId.get(taskId) === status) {
return res.json({ ok: true, duplicate: true });
}
lastStatusByTaskId.set(taskId, status);
console.log(
JSON.stringify(
{
task_id: taskId,
status,
files,
accepted: true,
},
null,
2,
),
);
// Keep the response path short. In production, persist the event first,
// then reconcile in a background worker before irreversible business actions.
void reconcileTaskStatus(taskId, { timeoutMs: reconcileTimeoutMs }).then((reconciled) => {
console.log(JSON.stringify({ task_id: taskId, reconciled }, null, 2));
});
return res.json({ ok: true });
});
app.listen(port, () => {
console.log(`APIDot webhook receiver listening on port ${port}`);
});