import { ActivityPerformanceSchema, ActivityVerificationsSchema, ActivityMediaKeysSchema, PlayerSchema, minToMs, Activity, } from "@blade-and-brawn/domain" import { cors } from "@elysiajs/cors"; import { Elysia, redirect, status, t } from "elysia"; import { PrintfulError, WebflowError, Printful, Webflow, } from "@blade-and-brawn/commerce"; import { AccountIdSchema, BigIntIdSchema, DEFAULT_NAME, DUMMY_PASSWORD_HASH, env, log } from "./util"; import serverTiming from "@elysia/server-timing"; import jwt from "@elysia/jwt"; import { CommerceService, WOrderStatusSchema } from "./services/commerce"; import cluster from "node:cluster"; import { randomUUIDv7, sleep, S3Client } from "bun"; import { DatabaseError } from "pg"; import { CalculatorService, CalculatorUnavailableError } from "./services/calculator"; import { StandardsParamsSchema } from "@blade-and-brawn/calculator"; import { StandardsService } from "./services/standards"; import { EventsService, EventStatusSchema } from "./services/events"; import { AccountsService, type AccountRole } from "./services/accounts"; import { Value } from "@sinclair/typebox/value"; import { AssessmentsService } from "./services/assessments"; import { VerificationsService, VerificationStatusSchema } from "./services/verifications"; // CONSTANTS // ----------------------- const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec const JWT_EXP = "1d"; const JWT_EXP_SECONDS = 60 * 60 * 24; // keep in sync with JWT_EXP; used for cookie maxAge // SERVICES // ----------------------- const s = (() => { const Standards = new StandardsService(); const Calculator = new CalculatorService(DEFAULT_NAME); const Commerce = new CommerceService(); const Accounts = new AccountsService(Calculator); const Events = new EventsService(); const Assessments = new AssessmentsService(); const Verifications = new VerificationsService(); return { Standards, Calculator, Commerce, Accounts, Events, Assessments, Verifications }; })(); // R2 BUCKETS // ----------------------- const r2 = { verificationMedia: new S3Client({ accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, bucket: "verification-media", endpoint: env.R2_URL, }) }; // QUEUES // ----------------------- const queues = [ s.Commerce.Apparel.Syncs.Queue, s.Commerce.Apparel.Orders.Queue, ] as const; // PLUGINS // ----------------------- const authPlugin = new Elysia({ name: "auth" }) .use(jwt({ name: "jwt", secret: env.AUTH_SECRET })) .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) }) .macro({ authAdmin: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); if (!token || token.role !== "admin" || !token.accountId || !token.sessionId) return status(401, { error: "Unauthorized" }); return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() }; } }, auth: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); if (!token || !token.role || !token.accountId || !token.sessionId) return status(401, { error: "Unauthorized" }); return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() }; } } }); // ELYSIA // ----------------------- export const app = new Elysia() .use(serverTiming()) .use( cors({ origin: [ // production /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i, // testing /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i, // development "http://localhost:5173", ], }), ) .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()), authDiscord: t.Optional(t.String()) }) }) .use(authPlugin) .error({ PrintfulError, WebflowError, CalculatorUnavailableError }) .onError(({ code, error }) => { // Database Errors if (error instanceof DatabaseError && error.code === "23505") return status(409, { error: "Conflicts with an existing record" }); // Custom Errors switch (code) { case "PrintfulError": case "WebflowError": log.error( { upstreamStatus: error.upstreamStatus, payload: error.payload }, error.message, ); return status(error.status, { error: error.message }); case "CalculatorUnavailableError": log.error({ err: error.cause }, error.message); return status(error.status, { error: error.message }); case "NOT_FOUND": return status(404, { error: error.message || "NOT_FOUND" }); case "VALIDATION": return status(error.status, { error: error.message }); default: log.error({ err: error }, "unhandled error"); } }) .onAfterResponse(({ request, set, path, responseValue }) => { if (env.NODE_ENV === "development") { const skip: Record = { "/commerce/products/sync/": ["GET"] }; if (skip[path]?.includes(request.method)) return; } const failed = Number(set.status) >= 400; log[failed ? "warn" : "info"]({ method: request.method, path, status: set.status, ...(failed && { response: responseValue }), }, "request"); }) .get("/", () => ({ status: "ok" })) .get("/health", () => ({ status: "ok" })) // AUTHENTICATION .post("/auth/login", async ({ jwt, body: { password, email }, cookie: { auth } }) => { const account = await s.Accounts.getByEmail(email); const password_match = await Bun.password.verify(password, account?.password_hash ?? DUMMY_PASSWORD_HASH); if (!account || !password_match) return status(401, { error: "Invalid credentials" }); auth.set({ value: await jwt.sign({ role: account.role, sessionId: randomUUIDv7(), accountId: account.id, exp: JWT_EXP }), path: "/", maxAge: JWT_EXP_SECONDS, sameSite: env.NODE_ENV === "production" ? "lax" : "none", httpOnly: true, secure: true, domain: env.NODE_ENV === "production" ? ".bladeandbrawn.com" : undefined }); }, { body: t.Object({ email: t.String(), password: t.String() }) }) .group("/auth/discord", (app) => app .get("/login", async ({ cookie: { authDiscord } }) => { const state = crypto.randomUUID(); authDiscord.set({ value: state, path: "/", maxAge: 60 * 10, // 10 min sameSite: "lax", httpOnly: true, secure: env.NODE_ENV === "production" }); const url = new URL("https://discord.com/api/oauth2/authorize"); url.searchParams.set("client_id", env.BOT_CLIENT_ID); url.searchParams.set("redirect_uri", env.BOT_REDIRECT_URL); url.searchParams.set("response_type", "code"); url.searchParams.set("scope", "identify email"); url.searchParams.set("state", state); return redirect(url.toString(), 302); }) .get("/callback", async ({ query, cookie: { authDiscord, auth }, jwt }) => { if (query.error) return status(400, { error: query.error_description ? `${query.error}: ${query.error_description}` : query.error }); if (!query.state) return status(400, { error: "No Discord OAuth2 state query parameter provided" }); if (query.state !== authDiscord.value) return status(400, { error: "Invalid Discord OAuth state" }); const tokenRes = await fetch("https://discord.com/api/oauth2/token", { method: "POST", headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: env.BOT_CLIENT_ID, client_secret: env.BOT_CLIENT_SECRET, grant_type: "authorization_code", code: query.code ?? "", redirect_uri: env.BOT_REDIRECT_URL }).toString() }); if (!tokenRes.ok) { const errorBody = await tokenRes.json().catch(() => null); return status(502, { error: errorBody ?? "Discord token exchange failed" }); } const tokenResPayload = await tokenRes.json(); Value.Assert(t.Object({ access_token: t.String() }), tokenResPayload); const identityRes = await fetch("https://discord.com/api/users/@me", { headers: { "Authorization": `Bearer ${tokenResPayload.access_token}` } }); if (!identityRes.ok) { const errorBody = await identityRes.json().catch(() => null); return status(502, { error: errorBody ?? "Failed to fetch Discord identity" }); } const identityResPayload = await identityRes.json(); Value.Assert(t.Object({ id: t.String(), email: t.Optional(t.String()), username: t.String() }), identityResPayload); // Create the account let accountId = (await s.Accounts.get(`@${identityResPayload.id}`))?.id; if (!accountId) { accountId = (await s.Accounts.create( identityResPayload.id, identityResPayload.email, identityResPayload.username )).id; } // Authenticate auth.set({ value: await jwt.sign({ role: "user" satisfies AccountRole, sessionId: randomUUIDv7(), accountId, exp: JWT_EXP }), path: "/", maxAge: JWT_EXP_SECONDS, sameSite: env.NODE_ENV === "production" ? "lax" : "none", httpOnly: true, secure: true, domain: env.NODE_ENV === "production" ? ".bladeandbrawn.com" : undefined }); return redirect(env.BOT_LOGIN_REDIRECT_URL, 302); }, { query: t.Object({ code: t.Optional(t.String()), state: t.Optional(t.String()), error: t.Optional(t.String()), error_description: t.Optional(t.String()), }) }) ) // CALCULATOR .group("/calculator", (app) => app // Non-authenticated .post("/calculate", async ({ body }) => { return { levels: await s.Calculator.calculate(body.player, body.activityPerformances) }; }, { body: t.Object({ player: PlayerSchema, activityPerformances: t.Array(ActivityPerformanceSchema) }), }) // Authenticated .guard({ authAdmin: true }, (app) => app .get("/standards/config", async () => { return await s.Calculator.Standards.Config.get(); }) .post("/standards/config/switch", async ({ body: { standardsConfigId } }) => { await s.Calculator.Standards.Config.switch(standardsConfigId); }, { body: t.Object({ standardsConfigId: BigIntIdSchema }) }) ) ) .group("/standards", { authAdmin: true }, (app) => app .post("/configs", async ({ body: { name, datasetId, params } }) => { return await s.Standards.Configs.create(name, datasetId, params); }, { body: t.Object({ name: t.String(), datasetId: BigIntIdSchema, params: StandardsParamsSchema }) }) .get("/configs", async () => { return await s.Standards.Configs.list(); }) .get("/configs/:id", async ({ params: { id } }) => { return await s.Standards.Configs.get(id); }, { params: t.Object({ id: BigIntIdSchema }) }) .put("/configs/:id", async ({ params: { id }, body: { name, datasetId, params: parameters } }) => { await s.Standards.Configs.update(id, name, datasetId, parameters); }, { params: t.Object({ id: BigIntIdSchema }), body: t.Object({ name: t.String(), datasetId: BigIntIdSchema, params: StandardsParamsSchema }) }) .delete("/configs/:id", async ({ params: { id } }) => { await s.Standards.Configs.delete(id); }, { params: t.Object({ id: BigIntIdSchema }) }) .get("/datasets", async () => { return await s.Standards.Datasets.list(); }) .get("/datasets/:id", async ({ params: { id } }) => { return await s.Standards.Datasets.get(id); }, { params: t.Object({ id: BigIntIdSchema }) }) .patch("/datasets/:id", async ({ params: { id }, body: { name } }) => { await s.Standards.Datasets.update(id, name); }, { params: t.Object({ id: BigIntIdSchema }), body: t.Object({ name: t.String() }) }) ) // COMMERCE .group("/commerce", { authAdmin: true }, (app) => app .group("/products", (app) => app .get("/", async ({ query }) => { const [pProducts, wProducts] = await Promise.all([ s.Commerce.Printful.Products.list({ limit: query.limit, offset: query.offset, }), s.Commerce.Webflow.Products.list({ forceAll: true }), ]); const wProductIds = new Set(wProducts.map((wProduct) => wProduct.product.id)); return pProducts.map((pProduct) => { const wProductId = pProduct.external_id.split("-")[0]; return { pProduct, isSynced: wProductId ? wProductIds.has(wProductId) : false }; }); }, { query: t.Object({ limit: t.Optional(t.Numeric({ maximum: 100 })), offset: t.Optional(t.Numeric()), }) }) .group("/sync", (app) => app // Sync status .get("/", async ({ sessionId }) => { const latestSyncState = await s.Commerce.Apparel.Syncs.getLatestSyncState(sessionId); if (!latestSyncState) return status(404, { error: "No product sync found for the provided session" }); return latestSyncState; }) // Run sync .post("/:pProductId?", async ({ params: { pProductId }, sessionId }) => { await s.Commerce.Apparel.Syncs.Queue.enqueue({ type: "apparel_sync_update", source: "portal", payload: { session: { id: sessionId, name: "Portal" }, filter: { pProductIds: pProductId ? [pProductId] : undefined } } }); }, { params: t.Object({ pProductId: t.Optional(t.Numeric()) }) }), ) .get("/:pProductId", async ({ params: { pProductId } }) => { const pProduct = await s.Commerce.Printful.Products.get(pProductId); if (!pProduct) return status(404, { error: "Missing printful product" }); const wProductId = pProduct.sync_product.external_id.split("-")[0]; if (!wProductId) return status(404, { error: "Missing webflow product ID" }); const wProduct = await s.Commerce.Webflow.Products.get(wProductId); return { pProduct, wProduct }; }, { params: t.Object({ pProductId: t.Numeric() }) }) ) .group("/orders", (app) => app .get("/", async ({ query }) => { const [wOrders, pOrders] = await Promise.all([ s.Commerce.Webflow.Orders.list({ status: query.status, limit: query.limit, offset: query.offset, }), s.Commerce.Printful.Orders.list({ forceAll: true }), ]); const pOrderExternalIds = new Set(pOrders.map((pOrder) => pOrder.external_id)); return wOrders.map((wOrder) => ({ wOrder, isSynced: pOrderExternalIds.has(wOrder.orderId), })); }, { query: t.Object({ status: t.Optional(WOrderStatusSchema), limit: t.Optional(t.Numeric({ maximum: 100 })), offset: t.Optional(t.Numeric()), }) }) .get("/:wOrderId", async ({ params: { wOrderId } }) => { const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId); if (!wOrder) return status(404, { error: "Missing webflow order" }); const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`); return { wOrder, pOrder }; }, { params: t.Object({ wOrderId: t.String() }) }) .post("/sync/:wOrderId", async ({ params: { wOrderId } }) => { const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId); if (!wOrder) return status(404, { error: "Missing webflow order" }); const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`); if (pOrder) return status(409, { error: "Cannot sync an already synced webflow order" }); await s.Commerce.Apparel.Orders.Queue.enqueue({ type: "apparel_order_create", source: "portal", payload: { wOrder } }); }, { params: t.Object({ wOrderId: t.String() }) }) ) ) // EVENTS .group("/events", { authAdmin: true }, (app) => app .get("/", async ({ query }) => { const events = await s.Events.list({ filter: { status: query.status, type: query.type, group: query.group }, limit: query.limit, offset: query.offset, }); return events.map((event) => ({ ...event, status: EventsService.status(event) })); }, { query: t.Object({ status: t.Optional(EventStatusSchema), type: t.Optional(t.String()), group: t.Optional(t.String()), limit: t.Optional(t.Numeric()), offset: t.Optional(t.Numeric()), }) }) .get("/:id", async ({ params: { id } }) => { const event = await s.Events.get(id); if (!event) return status(404, { error: "Event not found" }); return { ...event, status: EventsService.status(event) }; }, { params: t.Object({ id: BigIntIdSchema }) }) .post("/:id/retry", async ({ params: { id } }) => { const retried = await s.Events.retry(id); if (!retried) return status(404, { error: "Event not found or not in a failed state" }); }, { params: t.Object({ id: BigIntIdSchema }) }) .get("/groups", async ({ }) => queues.map((q) => q.group)) ) // ACCOUNTS .group("/accounts", (app) => app // TODO: for added security, could enforce bot or admin only access .get("/:id/stats", async ({ params: { id } }) => { const stats = await s.Accounts.stats(id); if (!stats) return status(404, { error: "Account stats not found" }); return stats; }, { params: t.Object({ id: AccountIdSchema }) }) .guard({ authAdmin: true }, (app) => app .get("/:id", async ({ params: { id } }) => { const account = await s.Accounts.get(id); if (!account) return status(404, { error: "Account not found" }); return account; }, { params: t.Object({ id: AccountIdSchema }) }) ) .group("/me", { auth: true }, (app) => app .get("/stats", async ({ accountId }) => { const stats = await s.Accounts.stats(accountId); if (!stats) return status(404, { error: "Account stats not found" }); return stats; }) .post("/assessments", async ({ body: { player, activityPerformances }, accountId }) => { return await s.Assessments.create(player, activityPerformances, accountId); }, { body: t.Object({ player: PlayerSchema, activityPerformances: t.Array(ActivityPerformanceSchema) }) }) // TODO: handle @ formatted accountIds .get("/assessments", async ({ accountId }) => { return await s.Assessments.list({ filter: { accountId } }); }) .delete("/assessments/:id", async ({ params: { id }, accountId }) => { const deleted = await s.Assessments.delete(id, accountId); if (!deleted) return status(404, { error: "Assessment not found" }); }, { params: t.Object({ id: BigIntIdSchema }) }) .post("/verifications", async ({ body: { assessmentId }, accountId }) => { const assessment = await s.Assessments.get(assessmentId, accountId); if (!assessment) return status(404, { error: "Assessment not found" }); // TODO: only allow a single active non-completed verification at a time const created = await s.Verifications.create(assessmentId); if (!created) return status(409, { error: "This assessment already has a verification" }); // Generate upload urls const activityMediaUrls: Record = {}; for (const activity of Object.values(Activity)) { activityMediaUrls[activity] = r2.verificationMedia.presign( `${assessmentId}/${activity}-${crypto.randomUUID()}`, { method: "PUT", expiresIn: 30 * 60 // 30 minutes }); } return { created, activityMediaUrls }; }, { body: t.Object({ assessmentId: BigIntIdSchema }) }) .patch("/verifications/:id", async ({ params: { id }, body: { assessmentId, activityMediaKeys }, accountId }) => { const verification = await s.Verifications.get(id, accountId); if (!verification) return status(404, { error: "Verification not found" }); if (assessmentId === undefined && Object.keys(activityMediaKeys ?? {}).length === 0) return status(400, { error: "Nothing to update" }); if (assessmentId !== undefined && !await s.Assessments.get(assessmentId, accountId)) return status(404, { error: "Assessment not found" }); const updated = await s.Verifications.update(id, { assessmentId, activityMediaKeys }); if (!updated) return status(409, { error: `Cannot update a ${verification.status} verification` }); return updated; }, { params: t.Object({ id: BigIntIdSchema }), body: t.Object({ assessmentId: t.Optional(BigIntIdSchema), activityMediaKeys: t.Optional(ActivityMediaKeysSchema), }) }) .post("/verifications/:id/submit", async ({ params: { id }, accountId }) => { if (!await s.Verifications.get(id, accountId)) return status(404, { error: "Verification not found" }); const submitted = await s.Verifications.submit(id); if (submitted) { // TODO: trigger discord webhook here return submitted; } const current = await s.Verifications.get(id, accountId); if (!current) return status(404, { error: "Verification not found" }); if (current.status === "submitted") return { id: current.id, status: current.status }; const missingActivityMediaKeys = VerificationsService.missingActivityMediaKeys(current); return status(409, { error: current.status === "draft" && missingActivityMediaKeys.length > 0 ? `Missing media for: ${missingActivityMediaKeys.join(", ")}` : `Cannot submit a ${current.status} verification` }); }, { params: t.Object({ id: BigIntIdSchema }) }) ) ) // ASSESSMENTS .group("/assessments", { authAdmin: true }, (app) => app .post("/", async ({ body: { player, activityPerformances, id } }) => { return await s.Assessments.create(player, activityPerformances, id); }, { body: t.Object({ player: PlayerSchema, activityPerformances: t.Array(ActivityPerformanceSchema), id: t.Optional(t.String()), }) }) .put("/:id", async ({ body: { player, activityPerformances }, params: { id } }) => { const updated = await s.Assessments.update(id, player, activityPerformances); if (!updated) return status(404, { error: "Assessment not found" }); return updated; }, { params: t.Object({ id: BigIntIdSchema }), body: t.Object({ player: PlayerSchema, activityPerformances: t.Array(ActivityPerformanceSchema), }) }) ) // VERIFICATIONS .group("/verifications", { authAdmin: true }, (app) => app .get("/", async ({ query }) => { return await s.Verifications.list({ filter: { status: query.status }, limit: query.limit, offset: query.offset, }); }, { query: t.Object({ status: t.Optional(VerificationStatusSchema), limit: t.Optional(t.Numeric()), offset: t.Optional(t.Numeric()), }) }) .get("/:id", async ({ params: { id } }) => { const verification = await s.Verifications.get(id); if (!verification) return status(404, { error: "Verification not found" }); return verification; }, { params: t.Object({ id: BigIntIdSchema }) }) .post("/:id/request-action", async ({ params: { id }, body: { reviewerNotes, activityVerifications } }) => { const updated = await s.Verifications.requestAction(id, reviewerNotes ?? null, activityVerifications); if (updated) return updated; const verification = await s.Verifications.get(id); if (!verification) return status(404, { error: "Verification not found" }); return status(409, { error: `Cannot request action on a ${verification.status} verification` }); }, { params: t.Object({ id: BigIntIdSchema }), body: t.Object({ reviewerNotes: t.Optional(t.String()), activityVerifications: ActivityVerificationsSchema, }) }) .post("/:id/complete", async ({ params: { id }, body: { activityVerifications }, accountId }) => { const updated = await s.Verifications.complete(id, accountId, activityVerifications); if (updated) return updated; const verification = await s.Verifications.get(id); if (!verification) return status(404, { error: "Verification not found" }); return status(409, { error: `Cannot complete a ${verification.status} verification` }); }, { params: t.Object({ id: BigIntIdSchema }), body: t.Object({ activityVerifications: ActivityVerificationsSchema, }) }) ) // WEBHOOKS .post("/webhooks/printful", async ({ body, query }) => { // https://webflow.com/integrations/printful if (!s.Commerce.Printful.Util.verifySecret(query.secret)) return status(400, { error: "Invalid secret" }); const payload = body as Printful.Webhook.EventPayload; switch (payload.type) { case Printful.Webhook.Event.ProductUpdated: { const pProduct = payload.data.sync_product; log.info({ productId: pProduct.id }, "printful webhook: product updated"); await s.Commerce.Apparel.Syncs.Queue.enqueue({ type: "apparel_sync_update", source: "printful", payload: { session: { id: randomUUIDv7(), name: "Printful" }, filter: { pProductIds: [pProduct.id] } } }); break; } case Printful.Webhook.Event.ProductDeleted: { const pProduct = payload.data.sync_product; const wProductId = pProduct.external_id.split("-")[0]; log.info({ externalId: payload.data.sync_product.external_id, wProductId }, "printful webhook: product deleted"); if (!wProductId) return status(404, { error: "Missing webflow product ID" }); await s.Commerce.Apparel.Syncs.Queue.enqueue({ type: "apparel_sync_delete", source: "printful", payload: { wProductId } }); break; } case Printful.Webhook.Event.PackageShipped: { const wOrderId = payload.data.order.external_id; const shipment = payload.data.shipment; log.info({ wOrderId, carrier: shipment.carrier, tracking: shipment.tracking_number }, "printful webhook: package shipped"); // TODO: possible to have multiple packages shipped // order should NOT be immediately fulfulled in that case await s.Commerce.Apparel.Orders.Queue.enqueue({ type: "apparel_order_fulfill", source: "printful", payload: { wOrderId, shipment } }); break; } default: log.warn({ type: (payload as any).type }, "printful webhook: unhandled event type"); } }, { query: t.Object({ secret: t.String() }) }) .post("/webhooks/webflow", async ({ request, body }) => { if (!s.Commerce.Webflow.Util.verifySecret(request, body)) return status(400, { error: "Invalid signature" }); const payload = body as Webflow.Webhook.EventPayload; switch (payload.triggerType) { case Webflow.Webhook.Event.OrderCreated: { const wOrder = payload.payload; log.info({ orderId: wOrder.orderId }, "webflow webhook: order created"); await s.Commerce.Apparel.Orders.Queue.enqueue({ type: "apparel_order_create", source: "webflow", payload: { wOrder } }); break; } default: log.warn({ triggerType: (payload as any).triggerType }, "webflow webhook: unhandled event type"); } }); app.listen(3000, async () => { if (cluster.worker?.id === 1) log.info({ port: 3000 }, "server started") // MANAGE QUEUES for (const queue of queues) { (async () => { while (true) { try { // clean, if ready if ((Date.now() - queue.lastCleanDate.getTime()) >= EventsService.CONCURRENCY_TIMEOUT_MS) await queue.clean().catch((err) => log.error({ name: queue.group, err })); // drain await queue.drain(); } catch (err) { log.error({ name: queue.group, err }, "error occurred during queue management"); } await sleep(EVENT_QUEUE_MANAGE_DELAY_MS); } })(); } }); export type API = typeof app export { type EventStatus } from "./services/events"; export { type VerificationStatus } from "./services/verifications";