🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add rate limiting to remaining endpoints

- File uploads: 20/hour
- User settings (name, avatar, notifications): 10 per 5 min
- Session deletion: 20/hour
- Passkey operations: 10 per 5 min (auth), 10/hour (update/delete)

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>

+63
+63
src/index.ts
··· 893 893 POST: async (req) => { 894 894 try { 895 895 const user = requireAuth(req); 896 + 897 + const rateLimitError = enforceRateLimit(req, "passkey-register-options", { 898 + ip: { max: 10, windowSeconds: 5 * 60 }, 899 + }); 900 + if (rateLimitError) return rateLimitError; 901 + 896 902 const options = await createRegistrationOptions(user); 897 903 return Response.json(options); 898 904 } catch (err) { ··· 904 910 POST: async (req) => { 905 911 try { 906 912 const _user = requireAuth(req); 913 + 914 + const rateLimitError = enforceRateLimit(req, "passkey-register-verify", { 915 + ip: { max: 10, windowSeconds: 5 * 60 }, 916 + }); 917 + if (rateLimitError) return rateLimitError; 918 + 907 919 const body = await req.json(); 908 920 const { response: credentialResponse, challenge, name } = body; 909 921 ··· 929 941 "/api/passkeys/authenticate/options": { 930 942 POST: async (req) => { 931 943 try { 944 + const rateLimitError = enforceRateLimit(req, "passkey-auth-options", { 945 + ip: { max: 10, windowSeconds: 5 * 60 }, 946 + }); 947 + if (rateLimitError) return rateLimitError; 948 + 932 949 const body = await req.json(); 933 950 const { email } = body; 934 951 ··· 942 959 "/api/passkeys/authenticate/verify": { 943 960 POST: async (req) => { 944 961 try { 962 + const rateLimitError = enforceRateLimit(req, "passkey-auth-verify", { 963 + ip: { max: 10, windowSeconds: 5 * 60 }, 964 + }); 965 + if (rateLimitError) return rateLimitError; 966 + 945 967 const body = await req.json(); 946 968 const { response: credentialResponse, challenge } = body; 947 969 ··· 1007 1029 PUT: async (req) => { 1008 1030 try { 1009 1031 const user = requireAuth(req); 1032 + 1033 + const rateLimitError = enforceRateLimit(req, "passkey-update", { 1034 + ip: { max: 10, windowSeconds: 60 * 60 }, 1035 + }); 1036 + if (rateLimitError) return rateLimitError; 1037 + 1010 1038 const body = await req.json(); 1011 1039 const { name } = body; 1012 1040 const passkeyId = req.params.id; ··· 1024 1052 DELETE: async (req) => { 1025 1053 try { 1026 1054 const user = requireAuth(req); 1055 + 1056 + const rateLimitError = enforceRateLimit(req, "passkey-delete", { 1057 + ip: { max: 10, windowSeconds: 60 * 60 }, 1058 + }); 1059 + if (rateLimitError) return rateLimitError; 1060 + 1027 1061 const passkeyId = req.params.id; 1028 1062 deletePasskey(passkeyId, user.id); 1029 1063 return Response.json({ success: true }); ··· 1063 1097 if (!user) { 1064 1098 return Response.json({ error: "Invalid session" }, { status: 401 }); 1065 1099 } 1100 + 1101 + const rateLimitError = enforceRateLimit(req, "delete-session", { 1102 + ip: { max: 20, windowSeconds: 60 * 60 }, 1103 + }); 1104 + if (rateLimitError) return rateLimitError; 1105 + 1066 1106 const body = await req.json(); 1067 1107 const targetSessionId = body.sessionId; 1068 1108 if (!targetSessionId) { ··· 1277 1317 if (!user) { 1278 1318 return Response.json({ error: "Invalid session" }, { status: 401 }); 1279 1319 } 1320 + 1321 + const rateLimitError = enforceRateLimit(req, "update-name", { 1322 + ip: { max: 10, windowSeconds: 5 * 60 }, 1323 + }); 1324 + if (rateLimitError) return rateLimitError; 1325 + 1280 1326 const body = await req.json(); 1281 1327 const { name } = body; 1282 1328 if (!name) { ··· 1303 1349 if (!user) { 1304 1350 return Response.json({ error: "Invalid session" }, { status: 401 }); 1305 1351 } 1352 + 1353 + const rateLimitError = enforceRateLimit(req, "update-avatar", { 1354 + ip: { max: 10, windowSeconds: 5 * 60 }, 1355 + }); 1356 + if (rateLimitError) return rateLimitError; 1357 + 1306 1358 const body = await req.json(); 1307 1359 const { avatar } = body; 1308 1360 if (!avatar) { ··· 1329 1381 if (!user) { 1330 1382 return Response.json({ error: "Invalid session" }, { status: 401 }); 1331 1383 } 1384 + 1385 + const rateLimitError = enforceRateLimit(req, "update-notifications", { 1386 + ip: { max: 10, windowSeconds: 5 * 60 }, 1387 + }); 1388 + if (rateLimitError) return rateLimitError; 1389 + 1332 1390 const body = await req.json(); 1333 1391 const { email_notifications_enabled } = body; 1334 1392 if (typeof email_notifications_enabled !== "boolean") { ··· 2015 2073 POST: async (req) => { 2016 2074 try { 2017 2075 const user = requireSubscription(req); 2076 + 2077 + const rateLimitError = enforceRateLimit(req, "upload-transcription", { 2078 + ip: { max: 20, windowSeconds: 60 * 60 }, 2079 + }); 2080 + if (rateLimitError) return rateLimitError; 2018 2081 2019 2082 const formData = await req.formData(); 2020 2083 const file = formData.get("audio") as File;