@oondemand/oon-core-back 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,171 +0,0 @@
1
- "use strict";
2
-
3
- const coreRegistry = require("../../core/registry");
4
- const GenericError = require("../../core/errors/GenericError");
5
-
6
- const providers = new Map();
7
-
8
- function normalizeKey(value, field = "identificador") {
9
- const key = String(value || "").trim().toLowerCase();
10
- if (!key) {
11
- throw new GenericError(`Informe o ${field} da integração.`, { statusCode: 500 });
12
- }
13
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(key)) {
14
- throw new GenericError(`${field} inválido: ${key}.`, { statusCode: 500 });
15
- }
16
- return key;
17
- }
18
-
19
- function normalizeResources(resources = []) {
20
- const seen = new Set();
21
- return resources.map((resource, index) => {
22
- const key = normalizeKey(resource.key, `identificador do recurso ${index + 1}`);
23
- if (seen.has(key)) {
24
- throw new GenericError(`Recurso duplicado no provedor: ${key}.`, { statusCode: 500 });
25
- }
26
- seen.add(key);
27
- return {
28
- ...resource,
29
- key,
30
- label: String(resource.label || key),
31
- order: Number(resource.order ?? (index + 1) * 10),
32
- includeInFullSync: resource.includeInFullSync !== false,
33
- };
34
- });
35
- }
36
-
37
- function publicConfiguration(configuration) {
38
- if (!configuration) return null;
39
- return {
40
- enabled: configuration.enabled !== false,
41
- multiInstance: configuration.multiInstance === true,
42
- fields: (configuration.fields || []).map((field) => ({
43
- key: field.key,
44
- label: field.label || field.key,
45
- type: field.type || "text",
46
- readOnly: field.readOnly === true,
47
- writeOnly: field.writeOnly === true,
48
- options: field.options || undefined,
49
- })),
50
- };
51
- }
52
-
53
- function metadataFor(provider) {
54
- return {
55
- id: provider.key,
56
- key: provider.key,
57
- name: provider.key,
58
- label: provider.label,
59
- description: provider.description || "",
60
- status: typeof provider.enabled === "function"
61
- ? (provider.enabled() ? "ativo" : "inativo")
62
- : (provider.enabled === false ? "inativo" : "ativo"),
63
- capabilities: [...(provider.capabilities || [])],
64
- configuration: publicConfiguration(provider.configuration),
65
- resources: provider.resources.map((resource) => ({
66
- key: resource.key,
67
- label: resource.label,
68
- description: resource.description || "",
69
- order: resource.order,
70
- includeInFullSync: resource.includeInFullSync,
71
- operations: [...(resource.operations || [resource.syncHandler ? "sync" : "execute"])],
72
- })),
73
- };
74
- }
75
-
76
- function registerIntegrationProvider(definition = {}) {
77
- const key = normalizeKey(definition.key, "identificador do provedor");
78
- if (providers.has(key) && definition.replace !== true) {
79
- throw new GenericError(`Provedor de integração já registrado: ${key}.`, { statusCode: 500 });
80
- }
81
-
82
- const provider = {
83
- ...definition,
84
- key,
85
- label: String(definition.label || key),
86
- resources: normalizeResources(definition.resources || []),
87
- handlers: { ...(definition.handlers || {}) },
88
- webhooks: { ...(definition.webhooks || {}) },
89
- maxAttempts: Math.max(1, Number(definition.maxAttempts || 5)),
90
- retryPolicy: {
91
- strategy: "exponential",
92
- initialMs: 60_000,
93
- maxMs: 21_600_000,
94
- jitter: true,
95
- ...(definition.retryPolicy || {}),
96
- },
97
- };
98
-
99
- providers.set(key, provider);
100
- coreRegistry.registerIntegration(key, metadataFor(provider));
101
- return provider;
102
- }
103
-
104
- function getIntegrationProvider(key) {
105
- const normalized = normalizeKey(key, "identificador do provedor");
106
- const provider = providers.get(normalized);
107
- if (!provider) {
108
- throw new GenericError(`Provedor de integração não registrado: ${normalized}.`, {
109
- statusCode: 404,
110
- });
111
- }
112
- return provider;
113
- }
114
-
115
- function listIntegrationProviders() {
116
- return [...providers.values()]
117
- .map(metadataFor)
118
- .sort((left, right) => left.label.localeCompare(right.label, "pt-BR"));
119
- }
120
-
121
- function resolveIntegrationHandler(providerKey, handlerKey) {
122
- const provider = getIntegrationProvider(providerKey);
123
- const key = String(handlerKey || "").trim();
124
- const handler = provider.handlers[key];
125
- if (!key || typeof handler !== "function") {
126
- throw new GenericError(
127
- `Operação ${key || "não informada"} não registrada para ${provider.label}.`,
128
- { statusCode: 422 },
129
- );
130
- }
131
- return { provider, handler, handlerKey: key };
132
- }
133
-
134
- function resolveWebhookHandler(providerKey, eventType) {
135
- const provider = getIntegrationProvider(providerKey);
136
- const type = String(eventType || "").trim();
137
- const handler = provider.webhooks[type] || provider.webhooks["*"] || provider.handleWebhook;
138
- if (typeof handler !== "function") {
139
- throw new GenericError(`Evento ${type || "não informado"} não suportado por ${provider.label}.`, {
140
- statusCode: 422,
141
- });
142
- }
143
- return { provider, handler, eventType: type };
144
- }
145
-
146
- function findResource(providerKey, resourceKey) {
147
- const provider = getIntegrationProvider(providerKey);
148
- const key = normalizeKey(resourceKey, "identificador do recurso");
149
- const resource = provider.resources.find((candidate) => candidate.key === key);
150
- if (!resource) {
151
- throw new GenericError(`Recurso ${key} não registrado para ${provider.label}.`, {
152
- statusCode: 404,
153
- });
154
- }
155
- return { provider, resource };
156
- }
157
-
158
- function resetIntegrationProviders() {
159
- providers.clear();
160
- }
161
-
162
- module.exports = {
163
- findResource,
164
- getIntegrationProvider,
165
- listIntegrationProviders,
166
- normalizeKey,
167
- registerIntegrationProvider,
168
- resetIntegrationProviders,
169
- resolveIntegrationHandler,
170
- resolveWebhookHandler,
171
- };
@@ -1,343 +0,0 @@
1
- "use strict";
2
-
3
- const express = require("express");
4
- const mongoose = require("mongoose");
5
- const GenericError = require("../../core/errors/GenericError");
6
- const auth = require("../../core/middlewares/auth");
7
- const { rbac } = require("../../core/middlewares/rbac");
8
- const { asyncHandler } = require("../../core/utils/helpers");
9
- const { getIntegrationModels, OUTBOX_STATUSES } = require("./models");
10
- const { getIntegrationProvider, listIntegrationProviders } = require("./registry");
11
- const {
12
- archiveIntegrationTicket,
13
- integrationHealth,
14
- processIntegrationQueue,
15
- processWebhookInbox,
16
- receiveWebhook,
17
- reprocessIntegrationTicket,
18
- synchronizeAll,
19
- synchronizeResource,
20
- } = require("./runtime");
21
- const { sanitizeIntegrationValue } = require("./sanitizer");
22
-
23
- const READ_ROLES = ["admin", "desenvolvedor"];
24
- const WRITE_ROLES = ["admin", "desenvolvedor"];
25
- const WEBHOOK_AUTO_PROCESS_LIMIT = 50;
26
- const webhookProvidersPending = new Set();
27
- let webhookProcessingScheduled = false;
28
-
29
- function scheduleWebhookProcessing(provider) {
30
- const providerKey = String(provider || "").trim().toLowerCase();
31
- if (!providerKey) return;
32
-
33
- webhookProvidersPending.add(providerKey);
34
- if (webhookProcessingScheduled) return;
35
- webhookProcessingScheduled = true;
36
-
37
- setImmediate(async () => {
38
- const providers = [...webhookProvidersPending];
39
- webhookProvidersPending.clear();
40
- try {
41
- for (const currentProvider of providers) {
42
- try {
43
- const events = await processWebhookInbox({
44
- provider: currentProvider,
45
- limit: WEBHOOK_AUTO_PROCESS_LIMIT,
46
- });
47
- const queue = await processIntegrationQueue({
48
- provider: currentProvider,
49
- limit: WEBHOOK_AUTO_PROCESS_LIMIT,
50
- });
51
-
52
- if (
53
- (Array.isArray(events?.results) && events.results.length >= WEBHOOK_AUTO_PROCESS_LIMIT)
54
- || (Array.isArray(queue?.results) && queue.results.length >= WEBHOOK_AUTO_PROCESS_LIMIT)
55
- ) {
56
- webhookProvidersPending.add(currentProvider);
57
- }
58
- } catch (error) {
59
- console.error(
60
- `[oon-integrations] Falha ao processar webhook automaticamente para ${currentProvider}: ${String(error?.message || error)}`,
61
- );
62
- }
63
- }
64
- } finally {
65
- webhookProcessingScheduled = false;
66
- if (webhookProvidersPending.size) {
67
- scheduleWebhookProcessing([...webhookProvidersPending][0]);
68
- }
69
- }
70
- });
71
- }
72
-
73
- function publicWebhookUrl(path) {
74
- const webhookPath = String(path || "").trim();
75
- const publicAppUrl = String(process.env.PUBLIC_APP_URL || "").trim();
76
- if (!webhookPath || !publicAppUrl) return webhookPath;
77
-
78
- try {
79
- const url = new URL(publicAppUrl);
80
- const basePath = url.pathname.replace(/\/+$/, "");
81
- const apiBasePath = basePath.endsWith("/api") ? basePath : `${basePath}/api`;
82
- const relativePath = webhookPath.startsWith("/") ? webhookPath : `/${webhookPath}`;
83
- url.pathname = `${apiBasePath}${relativePath}`.replace(/\/{2,}/g, "/");
84
- url.search = "";
85
- url.hash = "";
86
- return url.toString();
87
- } catch {
88
- return webhookPath;
89
- }
90
- }
91
-
92
- function number(value, fallback, max = 100) {
93
- const parsed = Number(value);
94
- return Number.isFinite(parsed) ? Math.min(max, Math.max(1, parsed)) : fallback;
95
- }
96
-
97
- function pagination(query = {}) {
98
- const limit = number(query.limit || query.pageSize, 20, 200);
99
- const page = number(query.page || query.pageIndex, 1, 100_000);
100
- return { limit, page, skip: (page - 1) * limit };
101
- }
102
-
103
- function escapeRegExp(value) {
104
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
105
- }
106
-
107
- function buildQueueFilter(query = {}, options = {}) {
108
- const includeStatus = options.includeStatus !== false;
109
- const filter = {};
110
- for (const field of ["provider", "resource", "operation", "aggregateType", "aggregateId"]) {
111
- if (query[field]) filter[field] = String(query[field]);
112
- }
113
- if (includeStatus && query.status) filter.status = String(query.status);
114
-
115
- const search = String(query.search || "").trim().slice(0, 160);
116
- if (search) {
117
- const pattern = new RegExp(escapeRegExp(search), "i");
118
- const clauses = [
119
- { handler: pattern },
120
- { resource: pattern },
121
- { operation: pattern },
122
- { aggregateType: pattern },
123
- { aggregateId: pattern },
124
- { idempotencyKey: pattern },
125
- ];
126
- if (mongoose.isValidObjectId(search)) clauses.unshift({ _id: new mongoose.Types.ObjectId(search) });
127
- filter.$or = clauses;
128
- }
129
- return filter;
130
- }
131
-
132
- async function queueStatusSummary(Outbox, filter = {}) {
133
- const rows = await Outbox.aggregate([
134
- { $match: filter },
135
- { $group: { _id: "$status", count: { $sum: 1 } } },
136
- ]);
137
- const statuses = Object.fromEntries(OUTBOX_STATUSES.map((status) => [status, 0]));
138
- for (const row of rows) statuses[String(row._id)] = Number(row.count || 0);
139
- return {
140
- total: Object.values(statuses).reduce((sum, count) => sum + Number(count || 0), 0),
141
- statuses,
142
- };
143
- }
144
-
145
- function requireProviderExtension(provider, method, message) {
146
- if (typeof provider[method] !== "function") {
147
- throw new GenericError(message || `O provedor ${provider.label} não oferece ${method}.`, {
148
- statusCode: 404,
149
- });
150
- }
151
- return provider[method].bind(provider);
152
- }
153
-
154
- function buildIntegrationRouter() {
155
- const router = express.Router();
156
-
157
- router.post("/webhooks/:provider/:token", asyncHandler(async (req, res) => {
158
- const event = await receiveWebhook({
159
- provider: req.params.provider,
160
- token: req.params.token,
161
- headers: req.headers,
162
- payload: req.body,
163
- resource: req.query.resource,
164
- eventType: req.body?.eventType || req.body?.event_type || req.body?.topic || "unknown",
165
- externalEventId: req.body?.eventId || req.body?.event_id || req.body?.id,
166
- });
167
- res.status(200).json({ accepted: true, id: String(event._id), duplicate: event.status !== "Pendente" });
168
- scheduleWebhookProcessing(req.params.provider);
169
- }));
170
-
171
- router.use(auth);
172
- router.get("/providers", rbac(READ_ROLES), (_req, res) => {
173
- res.json({ results: listIntegrationProviders() });
174
- });
175
- router.get("/providers/:provider/catalog", rbac(READ_ROLES), (req, res) => {
176
- const provider = getIntegrationProvider(req.params.provider);
177
- const metadata = listIntegrationProviders().find((item) => item.key === provider.key);
178
- res.json({ provider: provider.key, resources: metadata?.resources || [] });
179
- });
180
- router.get("/providers/:provider/configuration", rbac(READ_ROLES), asyncHandler(async (req, res) => {
181
- const provider = getIntegrationProvider(req.params.provider);
182
- const getConfiguration = requireProviderExtension(
183
- provider,
184
- "getConfiguration",
185
- `O provedor ${provider.label} não possui configuração gerenciada pelo Core.`,
186
- );
187
- res.json({ provider: provider.key, instances: sanitizeIntegrationValue(await getConfiguration()) });
188
- }));
189
- router.put("/providers/:provider/configuration/:instanceId", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
190
- const provider = getIntegrationProvider(req.params.provider);
191
- const saveConfiguration = requireProviderExtension(provider, "saveConfiguration");
192
- const saved = await saveConfiguration(req.params.instanceId, req.body || {});
193
- res.json({ provider: provider.key, configuration: sanitizeIntegrationValue(saved) });
194
- }));
195
- router.post("/providers/:provider/configuration/:instanceId/test", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
196
- const provider = getIntegrationProvider(req.params.provider);
197
- const testConnection = requireProviderExtension(provider, "testConnection");
198
- res.json(await testConnection(req.params.instanceId, {
199
- input: req.body || {},
200
- source: "api",
201
- requestId: req.requestId || "",
202
- }));
203
- }));
204
- router.get("/providers/:provider/configuration/:instanceId/webhook", rbac(READ_ROLES), asyncHandler(async (req, res) => {
205
- const provider = getIntegrationProvider(req.params.provider);
206
- const getWebhookAccess = requireProviderExtension(provider, "getWebhookAccess");
207
- const access = await getWebhookAccess(req.params.instanceId);
208
- res.json({ provider: provider.key, ...access, webhookPath: publicWebhookUrl(access.webhookPath) });
209
- }));
210
- router.post("/providers/:provider/configuration/:instanceId/webhook/rotate", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
211
- const provider = getIntegrationProvider(req.params.provider);
212
- const rotateWebhookToken = requireProviderExtension(provider, "rotateWebhookToken");
213
- const access = await rotateWebhookToken(req.params.instanceId);
214
- res.json({ provider: provider.key, ...access, webhookPath: publicWebhookUrl(access.webhookPath) });
215
- }));
216
- router.get("/providers/:provider/mappings", rbac(READ_ROLES), asyncHandler(async (req, res) => {
217
- const provider = getIntegrationProvider(req.params.provider);
218
- const describe = requireProviderExtension(provider, "describe");
219
- res.json({ provider: provider.key, ...sanitizeIntegrationValue(await describe()) });
220
- }));
221
- router.post("/providers/:provider/calls/:call", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
222
- const provider = getIntegrationProvider(req.params.provider);
223
- const executeCall = requireProviderExtension(provider, "executeCall");
224
- res.json(await executeCall(req.params.call, req.body?.input || req.body || {}, {
225
- instanceId: req.body?.instanceId || "default",
226
- source: "api",
227
- requestId: req.requestId || "",
228
- }));
229
- }));
230
- router.get("/health", rbac(READ_ROLES), asyncHandler(async (_req, res) => {
231
- res.json({ results: await integrationHealth() });
232
- }));
233
-
234
- router.get("/history", rbac(READ_ROLES), asyncHandler(async (req, res) => {
235
- const { Execution } = getIntegrationModels();
236
- const { limit, page, skip } = pagination(req.query);
237
- const filter = {};
238
- for (const field of ["provider", "resource", "operation", "status"]) {
239
- if (req.query[field]) filter[field] = String(req.query[field]);
240
- }
241
- const [results, totalItems] = await Promise.all([
242
- Execution.find(filter).sort({ startedAt: -1 }).skip(skip).limit(limit).lean(),
243
- Execution.countDocuments(filter),
244
- ]);
245
- res.json({
246
- results: sanitizeIntegrationValue(results),
247
- pagination: {
248
- currentPage: page,
249
- totalItems,
250
- totalPages: Math.ceil(totalItems / limit),
251
- itemsPerPage: limit,
252
- },
253
- });
254
- }));
255
-
256
- router.get("/queue", rbac(READ_ROLES), asyncHandler(async (req, res) => {
257
- const { Outbox } = getIntegrationModels();
258
- const { limit, page, skip } = pagination(req.query);
259
- const filter = buildQueueFilter(req.query);
260
- const summaryFilter = buildQueueFilter(req.query, { includeStatus: false });
261
- const [results, totalItems, summary] = await Promise.all([
262
- Outbox.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
263
- Outbox.countDocuments(filter),
264
- queueStatusSummary(Outbox, summaryFilter),
265
- ]);
266
- res.json({
267
- results: sanitizeIntegrationValue(results),
268
- summary,
269
- pagination: {
270
- currentPage: page,
271
- totalItems,
272
- totalPages: Math.ceil(totalItems / limit),
273
- itemsPerPage: limit,
274
- },
275
- });
276
- }));
277
-
278
- router.get("/events", rbac(READ_ROLES), asyncHandler(async (req, res) => {
279
- const { Inbox } = getIntegrationModels();
280
- const { limit, page, skip } = pagination(req.query);
281
- const filter = {};
282
- for (const field of ["provider", "resource", "eventType", "status"]) {
283
- if (req.query[field]) filter[field] = String(req.query[field]);
284
- }
285
- const [results, totalItems] = await Promise.all([
286
- Inbox.find(filter).sort({ receivedAt: -1 }).skip(skip).limit(limit).lean(),
287
- Inbox.countDocuments(filter),
288
- ]);
289
- res.json({
290
- results: sanitizeIntegrationValue(results),
291
- pagination: {
292
- currentPage: page,
293
- totalItems,
294
- totalPages: Math.ceil(totalItems / limit),
295
- itemsPerPage: limit,
296
- },
297
- });
298
- }));
299
-
300
- router.post("/providers/:provider/resources/:resource/sync", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
301
- const result = await synchronizeResource(req.params.provider, req.params.resource, {
302
- payload: req.body,
303
- metadata: { source: "api", requestId: req.requestId || "", user: req.usuario?.email || "" },
304
- });
305
- res.status(200).json(result);
306
- }));
307
- router.post("/providers/:provider/sync-all", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
308
- const result = await synchronizeAll(req.params.provider, {
309
- payload: req.body,
310
- metadata: { source: "api", requestId: req.requestId || "", user: req.usuario?.email || "" },
311
- });
312
- res.status(result.status === "Concluído" ? 200 : 207).json(result);
313
- }));
314
- router.post("/queue/process", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
315
- res.json(await processIntegrationQueue({ provider: req.body?.provider, limit: req.body?.limit }));
316
- }));
317
- router.post("/events/process", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
318
- res.json(await processWebhookInbox({ provider: req.body?.provider, limit: req.body?.limit }));
319
- }));
320
- router.post("/queue/:id/archive", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
321
- res.json(await archiveIntegrationTicket(req.params.id, req.body?.reason));
322
- }));
323
- router.post("/queue/:id/reprocess", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
324
- res.json(await reprocessIntegrationTicket(req.params.id));
325
- }));
326
- router.post("/actions/process-active", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
327
- const [queue, events] = await Promise.all([
328
- processIntegrationQueue({ limit: req.body?.limit }),
329
- processWebhookInbox({ limit: req.body?.eventLimit }),
330
- ]);
331
- res.json({ queue, events });
332
- }));
333
-
334
- return router;
335
- }
336
-
337
- module.exports = {
338
- buildIntegrationRouter,
339
- buildQueueFilter,
340
- publicWebhookUrl,
341
- queueStatusSummary,
342
- scheduleWebhookProcessing,
343
- };
@@ -1,134 +0,0 @@
1
- "use strict";
2
-
3
- const GenericError = require("../../../core/errors/GenericError");
4
- const { getIntegrationModels } = require("../models");
5
- const { getIntegrationProvider, resolveWebhookHandler } = require("../registry");
6
- const { sanitizeIntegrationError, sanitizeIntegrationValue } = require("../sanitizer");
7
- const { acquireNext, finalizeLease, leaseHeartbeat } = require("./lease");
8
- const { runTrackedOperation } = require("./tracking");
9
- const { DEFAULT_LEASE_MS, calculateBackoff, classifyError, isEnabled, payloadHash, workerId } = require("./utils");
10
-
11
- async function receiveWebhook(input) {
12
- const provider = getIntegrationProvider(input.provider);
13
- let verificationMetadata = {};
14
- if (typeof provider.verifyWebhook === "function") {
15
- const verification = await provider.verifyWebhook({
16
- token: input.token,
17
- headers: input.headers || {},
18
- payload: input.payload,
19
- });
20
- const accepted = verification === true || verification?.accepted === true;
21
- if (!accepted) throw new GenericError("Webhook de integração não autorizado.", { statusCode: 401 });
22
- verificationMetadata = sanitizeIntegrationValue(verification?.metadata || {});
23
- }
24
-
25
- const normalized = typeof provider.normalizeWebhook === "function"
26
- ? await provider.normalizeWebhook({ ...input, metadata: verificationMetadata })
27
- : input;
28
- const sanitizedPayload = sanitizeIntegrationValue(normalized.payload || {});
29
- const hash = payloadHash(sanitizedPayload);
30
- const externalEventId = String(normalized.externalEventId || "").trim();
31
- const deduplicationKey = `${provider.key}:${externalEventId || hash}`;
32
- const { Inbox } = getIntegrationModels();
33
- return Inbox.findOneAndUpdate(
34
- { deduplicationKey },
35
- {
36
- $setOnInsert: {
37
- provider: provider.key,
38
- resource: String(normalized.resource || ""),
39
- eventType: String(normalized.eventType || "unknown"),
40
- externalEventId,
41
- deduplicationKey,
42
- payloadHash: hash,
43
- payload: sanitizedPayload,
44
- metadata: sanitizeIntegrationValue({
45
- ...verificationMetadata,
46
- ...(normalized.metadata || {}),
47
- }),
48
- receivedAt: new Date(),
49
- status: "Pendente",
50
- attempts: 0,
51
- nextAttemptAt: new Date(),
52
- },
53
- },
54
- { upsert: true, new: true, setDefaultsOnInsert: true },
55
- ).lean();
56
- }
57
-
58
- async function processWebhookInbox(options = {}) {
59
- const { Inbox } = getIntegrationModels();
60
- const limit = Math.min(500, Math.max(1, Number(options.limit || 20)));
61
- const worker = options.worker || workerId("inbox");
62
- const leaseMs = Number(options.leaseMs || DEFAULT_LEASE_MS);
63
- const results = [];
64
-
65
- for (let index = 0; index < limit; index += 1) {
66
- const query = { status: { $in: ["Pendente", "Erro temporário", "Processando"] } };
67
- if (options.provider) query.provider = String(options.provider).trim().toLowerCase();
68
- const event = await acquireNext(Inbox, query, { worker, leaseMs });
69
- if (!event) break;
70
- const pulse = leaseHeartbeat(Inbox, event, options);
71
- pulse.start();
72
- let provider;
73
- try {
74
- const resolved = resolveWebhookHandler(event.provider, event.eventType);
75
- provider = resolved.provider;
76
- const handler = resolved.handler;
77
- if (!isEnabled(provider)) {
78
- throw new GenericError(`A integração ${provider.label} está desativada.`, { statusCode: 503 });
79
- }
80
- const tracked = await runTrackedOperation({
81
- provider: provider.key,
82
- resource: event.resource || "",
83
- operation: "webhook",
84
- title: `${provider.label}: webhook ${event.eventType}`,
85
- metadata: {
86
- source: "inbox",
87
- inboxId: String(event._id),
88
- externalEventId: event.externalEventId || "",
89
- ...sanitizeIntegrationValue(event.metadata || {}),
90
- },
91
- runner: (trace) => handler(event, {
92
- ...options,
93
- ...trace,
94
- provider,
95
- heartbeat: pulse.beat,
96
- sanitize: sanitizeIntegrationValue,
97
- }),
98
- });
99
- const committed = await finalizeLease(Inbox, event, {
100
- status: "Concluído",
101
- processedAt: new Date(),
102
- lastError: {},
103
- nextAttemptAt: null,
104
- });
105
- if (!committed) {
106
- throw new GenericError("Resultado do webhook ignorado porque o lease expirou.", { statusCode: 409 });
107
- }
108
- results.push({ id: String(event._id), status: "Concluído", response: tracked.result });
109
- } catch (error) {
110
- const effectiveProvider = provider || { maxAttempts: 1, retryPolicy: { jitter: false } };
111
- const classification = classifyError(effectiveProvider, error, Number(event.attempts || 1));
112
- const committed = await finalizeLease(Inbox, event, {
113
- status: classification.definitive ? "Erro definitivo" : "Erro temporário",
114
- lastError: sanitizeIntegrationError(error),
115
- nextAttemptAt: classification.definitive
116
- ? null
117
- : new Date(Date.now() + calculateBackoff(event.attempts, effectiveProvider.retryPolicy)),
118
- });
119
- if (!committed) {
120
- throw new GenericError("Erro do webhook ignorado porque o lease expirou.", { statusCode: 409 });
121
- }
122
- results.push({
123
- id: String(event._id),
124
- status: classification.definitive ? "Erro definitivo" : "Erro temporário",
125
- error: sanitizeIntegrationError(error),
126
- });
127
- } finally {
128
- pulse.stop();
129
- }
130
- }
131
- return { processed: results.filter((item) => item.status === "Concluído").length, results };
132
- }
133
-
134
- module.exports = { processWebhookInbox, receiveWebhook };