🪻 distributed transcription service thistle.dunkirk.sh

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>

dunkirk.sh 07dd273c 4dc7250a

verified
Changed files
+63
src
+63
src/index.ts
···
POST: async (req) => {
try {
const user = requireAuth(req);
+
+
const rateLimitError = enforceRateLimit(req, "passkey-register-options", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const options = await createRegistrationOptions(user);
return Response.json(options);
} catch (err) {
···
POST: async (req) => {
try {
const _user = requireAuth(req);
+
+
const rateLimitError = enforceRateLimit(req, "passkey-register-verify", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { response: credentialResponse, challenge, name } = body;
···
"/api/passkeys/authenticate/options": {
POST: async (req) => {
try {
+
const rateLimitError = enforceRateLimit(req, "passkey-auth-options", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { email } = body;
···
"/api/passkeys/authenticate/verify": {
POST: async (req) => {
try {
+
const rateLimitError = enforceRateLimit(req, "passkey-auth-verify", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { response: credentialResponse, challenge } = body;
···
PUT: async (req) => {
try {
const user = requireAuth(req);
+
+
const rateLimitError = enforceRateLimit(req, "passkey-update", {
+
ip: { max: 10, windowSeconds: 60 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { name } = body;
const passkeyId = req.params.id;
···
DELETE: async (req) => {
try {
const user = requireAuth(req);
+
+
const rateLimitError = enforceRateLimit(req, "passkey-delete", {
+
ip: { max: 10, windowSeconds: 60 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const passkeyId = req.params.id;
deletePasskey(passkeyId, user.id);
return Response.json({ success: true });
···
if (!user) {
return Response.json({ error: "Invalid session" }, { status: 401 });
+
+
const rateLimitError = enforceRateLimit(req, "delete-session", {
+
ip: { max: 20, windowSeconds: 60 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const targetSessionId = body.sessionId;
if (!targetSessionId) {
···
if (!user) {
return Response.json({ error: "Invalid session" }, { status: 401 });
+
+
const rateLimitError = enforceRateLimit(req, "update-name", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { name } = body;
if (!name) {
···
if (!user) {
return Response.json({ error: "Invalid session" }, { status: 401 });
+
+
const rateLimitError = enforceRateLimit(req, "update-avatar", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { avatar } = body;
if (!avatar) {
···
if (!user) {
return Response.json({ error: "Invalid session" }, { status: 401 });
+
+
const rateLimitError = enforceRateLimit(req, "update-notifications", {
+
ip: { max: 10, windowSeconds: 5 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
+
const body = await req.json();
const { email_notifications_enabled } = body;
if (typeof email_notifications_enabled !== "boolean") {
···
POST: async (req) => {
try {
const user = requireSubscription(req);
+
+
const rateLimitError = enforceRateLimit(req, "upload-transcription", {
+
ip: { max: 20, windowSeconds: 60 * 60 },
+
});
+
if (rateLimitError) return rateLimitError;
const formData = await req.formData();
const file = formData.get("audio") as File;