···
import { SlackCache } from "./cache";
import { SlackWrapper } from "./slackWrapper";
import { getEmojiUrl } from "../utils/emojiHelper";
6
-
import type { SlackUser } from "./slack";
7
-
import swaggerSpec from "./swagger";
6
+
import { createApiRoutes } from "./routes/api-routes";
7
+
import { buildRoutes, getSwaggerSpec } from "./lib/route-builder";
import dashboard from "./dashboard.html";
import swagger from "./swagger.html";
···
environment: process.env.NODE_ENV,
dsn: process.env.SENTRY_DSN,
19
-
// Ignore all 404-related errors
18
+
ignoreErrors: ["Not Found", "404", "user_not_found", "emoji_not_found"],
console.warn("Sentry DSN not provided, error monitoring is disabled");
···
const emojiEntries = Object.entries(emojis)
if (typeof url === "string" && url.startsWith("alias:")) {
41
-
const aliasName = url.substring(6); // Remove 'alias:' prefix
35
+
const aliasName = url.substring(6);
const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName) ?? null;
···
// Inject SlackWrapper into cache for background user updates
cache.setSlackWrapper(slackApp);
77
-
// Cache maintenance is now handled automatically by cache.ts scheduled tasks
80
-
const server = serve({
83
-
"/dashboard": dashboard,
84
-
"/swagger": swagger,
85
-
"/swagger.json": async (request) => {
86
-
return Response.json(swaggerSpec);
88
-
"/favicon.ico": async (request) => {
89
-
return new Response(Bun.file("./favicon.ico"));
92
-
// Root route - redirect to dashboard for browsers
93
-
"/": async (request) => {
94
-
const startTime = Date.now();
95
-
const recordAnalytics = async (statusCode: number) => {
96
-
const userAgent = request.headers.get("user-agent") || "";
98
-
request.headers.get("x-forwarded-for") ||
99
-
request.headers.get("x-real-ip") ||
102
-
await cache.recordRequest(
108
-
Date.now() - startTime,
112
-
const userAgent = request.headers.get("user-agent") || "";
114
-
userAgent.toLowerCase().includes("mozilla") ||
115
-
userAgent.toLowerCase().includes("chrome") ||
116
-
userAgent.toLowerCase().includes("safari")
118
-
recordAnalytics(302);
119
-
return new Response(null, {
121
-
headers: { Location: "/dashboard" },
71
+
// Create the typed API routes with injected dependencies
72
+
const apiRoutes = createApiRoutes(cache, slackApp);
125
-
recordAnalytics(200);
126
-
return new Response(
127
-
"Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---",
74
+
// Build Bun-compatible routes and generate Swagger
75
+
const typedRoutes = buildRoutes(apiRoutes);
76
+
const generatedSwagger = getSwaggerSpec();
131
-
// Health check endpoint
133
-
async GET(request) {
134
-
const startTime = Date.now();
135
-
const recordAnalytics = async (statusCode: number) => {
136
-
const userAgent = request.headers.get("user-agent") || "";
138
-
request.headers.get("x-forwarded-for") ||
139
-
request.headers.get("x-real-ip") ||
142
-
await cache.recordRequest(
148
-
Date.now() - startTime,
152
-
return handleHealthCheck(request, recordAnalytics);
158
-
async GET(request) {
159
-
const startTime = Date.now();
160
-
const recordAnalytics = async (statusCode: number) => {
161
-
const userAgent = request.headers.get("user-agent") || "";
163
-
request.headers.get("x-forwarded-for") ||
164
-
request.headers.get("x-real-ip") ||
167
-
await cache.recordRequest(
173
-
Date.now() - startTime,
177
-
return handleGetUser(request, recordAnalytics);
182
-
async GET(request) {
183
-
const startTime = Date.now();
184
-
const recordAnalytics = async (statusCode: number) => {
185
-
const userAgent = request.headers.get("user-agent") || "";
187
-
request.headers.get("x-forwarded-for") ||
188
-
request.headers.get("x-real-ip") ||
191
-
await cache.recordRequest(
197
-
Date.now() - startTime,
201
-
return handleUserRedirect(request, recordAnalytics);
205
-
"/users/:id/purge": {
206
-
async POST(request) {
207
-
const startTime = Date.now();
208
-
const recordAnalytics = async (statusCode: number) => {
209
-
const userAgent = request.headers.get("user-agent") || "";
211
-
request.headers.get("x-forwarded-for") ||
212
-
request.headers.get("x-real-ip") ||
215
-
await cache.recordRequest(
221
-
Date.now() - startTime,
225
-
return handlePurgeUser(request, recordAnalytics);
231
-
async GET(request) {
232
-
const startTime = Date.now();
233
-
const recordAnalytics = async (statusCode: number) => {
234
-
const userAgent = request.headers.get("user-agent") || "";
236
-
request.headers.get("x-forwarded-for") ||
237
-
request.headers.get("x-real-ip") ||
240
-
await cache.recordRequest(
246
-
Date.now() - startTime,
250
-
return handleListEmojis(request, recordAnalytics);
255
-
async GET(request) {
256
-
const startTime = Date.now();
257
-
const recordAnalytics = async (statusCode: number) => {
258
-
const userAgent = request.headers.get("user-agent") || "";
260
-
request.headers.get("x-forwarded-for") ||
261
-
request.headers.get("x-real-ip") ||
264
-
await cache.recordRequest(
270
-
Date.now() - startTime,
274
-
return handleGetEmoji(request, recordAnalytics);
278
-
"/emojis/:name/r": {
279
-
async GET(request) {
280
-
const startTime = Date.now();
281
-
const recordAnalytics = async (statusCode: number) => {
282
-
const userAgent = request.headers.get("user-agent") || "";
284
-
request.headers.get("x-forwarded-for") ||
285
-
request.headers.get("x-real-ip") ||
288
-
await cache.recordRequest(
294
-
Date.now() - startTime,
298
-
return handleEmojiRedirect(request, recordAnalytics);
302
-
// Reset cache endpoint
304
-
async POST(request) {
305
-
const startTime = Date.now();
306
-
const recordAnalytics = async (statusCode: number) => {
307
-
const userAgent = request.headers.get("user-agent") || "";
309
-
request.headers.get("x-forwarded-for") ||
310
-
request.headers.get("x-real-ip") ||
313
-
await cache.recordRequest(
319
-
Date.now() - startTime,
323
-
return handleResetCache(request, recordAnalytics);
327
-
// Fast essential stats endpoint - loads immediately
328
-
"/api/stats/essential": {
329
-
async GET(request) {
330
-
const startTime = Date.now();
331
-
const recordAnalytics = async (statusCode: number) => {
332
-
const userAgent = request.headers.get("user-agent") || "";
334
-
request.headers.get("x-forwarded-for") ||
335
-
request.headers.get("x-real-ip") ||
338
-
await cache.recordRequest(
339
-
"/api/stats/essential",
344
-
Date.now() - startTime,
348
-
return handleGetEssentialStats(request, recordAnalytics);
352
-
// Chart data endpoint - loads after essential stats
353
-
"/api/stats/charts": {
354
-
async GET(request) {
355
-
const startTime = Date.now();
356
-
const recordAnalytics = async (statusCode: number) => {
357
-
const userAgent = request.headers.get("user-agent") || "";
359
-
request.headers.get("x-forwarded-for") ||
360
-
request.headers.get("x-real-ip") ||
363
-
await cache.recordRequest(
364
-
"/api/stats/charts",
369
-
Date.now() - startTime,
373
-
return handleGetChartData(request, recordAnalytics);
377
-
// User agents endpoint - loads last
378
-
"/api/stats/useragents": {
379
-
async GET(request) {
380
-
const startTime = Date.now();
381
-
const recordAnalytics = async (statusCode: number) => {
382
-
const userAgent = request.headers.get("user-agent") || "";
384
-
request.headers.get("x-forwarded-for") ||
385
-
request.headers.get("x-real-ip") ||
388
-
await cache.recordRequest(
389
-
"/api/stats/useragents",
394
-
Date.now() - startTime,
398
-
return handleGetUserAgents(request, recordAnalytics);
402
-
// Original stats endpoint (for backwards compatibility)
404
-
async GET(request) {
405
-
const startTime = Date.now();
406
-
const recordAnalytics = async (statusCode: number) => {
407
-
const userAgent = request.headers.get("user-agent") || "";
409
-
request.headers.get("x-forwarded-for") ||
410
-
request.headers.get("x-real-ip") ||
413
-
await cache.recordRequest(
419
-
Date.now() - startTime,
423
-
return handleGetStats(request, recordAnalytics);
78
+
// Legacy routes (non-API)
79
+
const legacyRoutes = {
80
+
"/dashboard": dashboard,
81
+
"/swagger": swagger,
82
+
"/swagger.json": async (request: Request) => {
83
+
return Response.json(generatedSwagger);
428
-
// Enable development mode for hot reloading
85
+
"/favicon.ico": async (request: Request) => {
86
+
return new Response(Bun.file("./favicon.ico"));
434
-
// Fallback fetch handler for unmatched routes and error handling
435
-
async fetch(request) {
436
-
const url = new URL(request.url);
437
-
const path = url.pathname;
438
-
const method = request.method;
439
-
const startTime = Date.now();
89
+
// Root route - redirect to dashboard for browsers
90
+
"/": async (request: Request) => {
91
+
const userAgent = request.headers.get("user-agent") || "";
441
-
// Record request analytics (except for favicon and swagger)
442
-
const recordAnalytics = async (statusCode: number) => {
443
-
if (path !== "/favicon.ico" && !path.startsWith("/swagger")) {
444
-
const userAgent = request.headers.get("user-agent") || "";
446
-
request.headers.get("x-forwarded-for") ||
447
-
request.headers.get("x-real-ip") ||
450
-
await cache.recordRequest(
456
-
Date.now() - startTime,
463
-
recordAnalytics(404);
464
-
return new Response("Not Found", { status: 404 });
467
-
`\x1b[31m x\x1b[0m unhandled error: \x1b[31m${error instanceof Error ? error.message : String(error)}\x1b[0m`,
470
-
// Don't send 404 errors to Sentry
472
-
error instanceof Error &&
473
-
(error.message === "Not Found" ||
474
-
error.message === "user_not_found" ||
475
-
error.message === "emoji_not_found");
477
-
if (!is404 && error instanceof Error) {
478
-
Sentry.withScope((scope) => {
479
-
scope.setExtra("url", request.url);
480
-
Sentry.captureException(error);
484
-
recordAnalytics(500);
485
-
return new Response("Internal Server Error", { status: 500 });
489
-
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
493
-
`\n---\n\n🐰 Bun server is running at ${server.url} on ${process.env.NODE_ENV}\n\n---\n`,
496
-
// Handler functions
497
-
async function handleHealthCheck(
499
-
recordAnalytics: (statusCode: number) => Promise<void>,
501
-
const slackConnection = await slackApp.testAuth();
502
-
const databaseConnection = await cache.healthCheck();
504
-
if (!slackConnection || !databaseConnection) {
505
-
await recordAnalytics(500);
506
-
return Response.json(
509
-
slack: slackConnection,
510
-
database: databaseConnection,
516
-
await recordAnalytics(200);
517
-
return Response.json({
524
-
async function handleGetUser(
526
-
recordAnalytics: (statusCode: number) => Promise<void>,
528
-
const url = new URL(request.url);
529
-
const userId = url.pathname.split("/").pop() || "";
530
-
const user = await cache.getUser(userId);
532
-
// If not found then check slack first
533
-
if (!user || !user.imageUrl) {
534
-
let slackUser: SlackUser;
536
-
slackUser = await slackApp.getUserInfo(userId);
538
-
if (e instanceof Error && e.message === "user_not_found") {
539
-
await recordAnalytics(404);
540
-
return Response.json({ message: "User not found" }, { status: 404 });
543
-
Sentry.withScope((scope) => {
544
-
scope.setExtra("url", request.url);
545
-
scope.setExtra("user", userId);
546
-
Sentry.captureException(e);
94
+
userAgent.toLowerCase().includes("mozilla") ||
95
+
userAgent.toLowerCase().includes("chrome") ||
96
+
userAgent.toLowerCase().includes("safari")
98
+
return new Response(null, {
100
+
headers: { Location: "/dashboard" },
549
-
if (e instanceof Error)
551
-
`\x1b[38;5;214m ⚠️ WARN\x1b[0m error on fetching user from slack: \x1b[38;5;208m${e.message}\x1b[0m`,
554
-
await recordAnalytics(500);
555
-
return Response.json(
556
-
{ message: `Error fetching user from Slack: ${e}` },
561
-
const displayName =
562
-
slackUser.profile.display_name_normalized ||
563
-
slackUser.profile.real_name_normalized;
565
-
await cache.insertUser(
568
-
slackUser.profile.pronouns,
569
-
slackUser.profile.image_512,
104
+
return new Response(
105
+
"Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---",
572
-
await recordAnalytics(200);
573
-
return Response.json({
575
-
expiration: new Date().toISOString(),
576
-
user: slackUser.id,
577
-
displayName: displayName,
578
-
pronouns: slackUser.profile.pronouns || null,
579
-
image: slackUser.profile.image_512,
583
-
await recordAnalytics(200);
584
-
return Response.json({
586
-
expiration: user.expiration.toISOString(),
588
-
displayName: user.displayName,
589
-
pronouns: user.pronouns,
590
-
image: user.imageUrl,
594
-
async function handleUserRedirect(
596
-
recordAnalytics: (statusCode: number) => Promise<void>,
598
-
const url = new URL(request.url);
599
-
const parts = url.pathname.split("/");
600
-
const userId = parts[2] || "";
601
-
const user = await cache.getUser(userId);
603
-
// If not found then check slack first
604
-
if (!user || !user.imageUrl) {
605
-
let slackUser: SlackUser;
607
-
slackUser = await slackApp.getUserInfo(userId.toUpperCase());
609
-
if (e instanceof Error && e.message === "user_not_found") {
611
-
`\x1b[38;5;214m ⚠️ WARN\x1b[0m user not found: \x1b[38;5;208m${userId}\x1b[0m`,
614
-
await recordAnalytics(307);
615
-
return new Response(null, {
619
-
"https://api.dicebear.com/9.x/thumbs/svg?seed={username_hash}",
624
-
Sentry.withScope((scope) => {
625
-
scope.setExtra("url", request.url);
626
-
scope.setExtra("user", userId);
627
-
Sentry.captureException(e);
630
-
if (e instanceof Error)
632
-
`\x1b[38;5;214m ⚠️ WARN\x1b[0m error on fetching user from slack: \x1b[38;5;208m${e.message}\x1b[0m`,
635
-
await recordAnalytics(500);
636
-
return Response.json(
637
-
{ message: `Error fetching user from Slack: ${e}` },
642
-
await cache.insertUser(
644
-
slackUser.profile.display_name_normalized ||
645
-
slackUser.profile.real_name_normalized,
646
-
slackUser.profile.pronouns,
647
-
slackUser.profile.image_512,
650
-
await recordAnalytics(302);
651
-
return new Response(null, {
653
-
headers: { Location: slackUser.profile.image_512 },
657
-
await recordAnalytics(302);
658
-
return new Response(null, {
660
-
headers: { Location: user.imageUrl },
664
-
async function handleListEmojis(
666
-
recordAnalytics: (statusCode: number) => Promise<void>,
668
-
const emojis = await cache.listEmojis();
670
-
await recordAnalytics(200);
671
-
return Response.json(
672
-
emojis.map((emoji) => ({
674
-
expiration: emoji.expiration.toISOString(),
676
-
...(emoji.alias ? { alias: emoji.alias } : {}),
677
-
image: emoji.imageUrl,
682
-
async function handleGetEmoji(
684
-
recordAnalytics: (statusCode: number) => Promise<void>,
686
-
const url = new URL(request.url);
687
-
const emojiName = url.pathname.split("/").pop() || "";
688
-
const emoji = await cache.getEmoji(emojiName);
691
-
const fallbackUrl = getEmojiUrl(emojiName);
692
-
if (!fallbackUrl) {
693
-
await recordAnalytics(404);
694
-
return Response.json({ message: "Emoji not found" }, { status: 404 });
697
-
await recordAnalytics(200);
698
-
return Response.json({
700
-
expiration: new Date().toISOString(),
702
-
image: fallbackUrl,
706
-
await recordAnalytics(200);
707
-
return Response.json({
709
-
expiration: emoji.expiration.toISOString(),
711
-
...(emoji.alias ? { alias: emoji.alias } : {}),
712
-
image: emoji.imageUrl,
716
-
async function handleEmojiRedirect(
718
-
recordAnalytics: (statusCode: number) => Promise<void>,
720
-
const url = new URL(request.url);
721
-
const parts = url.pathname.split("/");
722
-
const emojiName = parts[2] || "";
723
-
const emoji = await cache.getEmoji(emojiName);
726
-
const fallbackUrl = getEmojiUrl(emojiName);
727
-
if (!fallbackUrl) {
728
-
await recordAnalytics(404);
729
-
return Response.json({ message: "Emoji not found" }, { status: 404 });
732
-
await recordAnalytics(302);
733
-
return new Response(null, {
735
-
headers: { Location: fallbackUrl },
739
-
await recordAnalytics(302);
740
-
return new Response(null, {
742
-
headers: { Location: emoji.imageUrl },
746
-
async function handleResetCache(
748
-
recordAnalytics: (statusCode: number) => Promise<void>,
750
-
const authHeader = request.headers.get("authorization") || "";
752
-
if (authHeader !== `Bearer ${process.env.BEARER_TOKEN}`) {
753
-
await recordAnalytics(401);
754
-
return new Response("Unauthorized", { status: 401 });
757
-
const result = await cache.purgeAll();
758
-
await recordAnalytics(200);
759
-
return Response.json(result);
110
+
// Merge all routes
111
+
const allRoutes = {
762
-
async function handlePurgeUser(
764
-
recordAnalytics: (statusCode: number) => Promise<void>,
766
-
const authHeader = request.headers.get("authorization") || "";
116
+
// Start the server
117
+
const server = serve({
119
+
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
768
-
if (authHeader !== `Bearer ${process.env.BEARER_TOKEN}`) {
769
-
await recordAnalytics(401);
770
-
return new Response("Unauthorized", { status: 401 });
122
+
console.log(`🚀 Server running on http://localhost:${server.port}`);
773
-
const url = new URL(request.url);
774
-
const parts = url.pathname.split("/");
775
-
const userId = parts[2] || "";
776
-
const success = await cache.purgeUserCache(userId);
778
-
await recordAnalytics(200);
779
-
return Response.json({
780
-
message: success ? "User cache purged" : "User not found in cache",
786
-
async function handleGetStats(
788
-
recordAnalytics: (statusCode: number) => Promise<void>,
790
-
const url = new URL(request.url);
791
-
const params = new URLSearchParams(url.search);
792
-
const days = params.get("days") ? parseInt(params.get("days")!) : 7;
793
-
const analytics = await cache.getAnalytics(days);
795
-
await recordAnalytics(200);
796
-
return Response.json(analytics);
799
-
// Fast essential stats - just the 3 key metrics
800
-
async function handleGetEssentialStats(
802
-
recordAnalytics: (statusCode: number) => Promise<void>,
804
-
const url = new URL(request.url);
805
-
const params = new URLSearchParams(url.search);
806
-
const days = params.get("days") ? parseInt(params.get("days")!) : 7;
808
-
const essentialStats = await cache.getEssentialStats(days);
810
-
await recordAnalytics(200);
811
-
return Response.json(essentialStats);
814
-
// Chart data - requests and latency over time
815
-
async function handleGetChartData(
817
-
recordAnalytics: (statusCode: number) => Promise<void>,
819
-
const url = new URL(request.url);
820
-
const params = new URLSearchParams(url.search);
821
-
const days = params.get("days") ? parseInt(params.get("days")!) : 7;
823
-
const chartData = await cache.getChartData(days);
825
-
await recordAnalytics(200);
826
-
return Response.json(chartData);
829
-
// User agents data - slowest loading part
830
-
async function handleGetUserAgents(
832
-
recordAnalytics: (statusCode: number) => Promise<void>,
834
-
const url = new URL(request.url);
835
-
const params = new URLSearchParams(url.search);
836
-
const days = params.get("days") ? parseInt(params.get("days")!) : 7;
838
-
const userAgents = await cache.getUserAgents(days);
840
-
await recordAnalytics(200);
841
-
return Response.json(userAgents);
844
-
// Cache maintenance is now handled by scheduled tasks in cache.ts
845
-
// No aggressive daily purge needed - users will lazy load with longer TTL
124
+
export { cache, slackApp };