feat(core-api): harden signed urls and llm rate limits

This commit is contained in:
zouantchaw
2026-02-24 10:17:48 -05:00
parent e733f36d28
commit 52c3fbad40
7 changed files with 162 additions and 13 deletions

View File

@@ -0,0 +1,41 @@
const counters = new Map();
function currentWindowKey(now = Date.now()) {
return Math.floor(now / 60000);
}
function perMinuteLimit() {
return Number.parseInt(process.env.LLM_RATE_LIMIT_PER_MINUTE || '20', 10);
}
export function checkLlmRateLimit({ uid, now = Date.now() }) {
const limit = perMinuteLimit();
if (!Number.isFinite(limit) || limit <= 0) {
return { allowed: true, remaining: null, retryAfterSeconds: 0 };
}
const windowKey = currentWindowKey(now);
const record = counters.get(uid);
if (!record || record.windowKey !== windowKey) {
counters.set(uid, { windowKey, count: 1 });
return { allowed: true, remaining: limit - 1, retryAfterSeconds: 0 };
}
if (record.count >= limit) {
const retryAfterSeconds = (windowKey + 1) * 60 - Math.floor(now / 1000);
return {
allowed: false,
remaining: 0,
retryAfterSeconds: Math.max(1, retryAfterSeconds),
};
}
record.count += 1;
counters.set(uid, record);
return { allowed: true, remaining: limit - record.count, retryAfterSeconds: 0 };
}
export function __resetLlmRateLimitForTests() {
counters.clear();
}

View File

@@ -3,7 +3,7 @@ import { AppError } from '../lib/errors.js';
const storage = new Storage();
function parseGsUri(fileUri) {
export function parseGsUri(fileUri) {
if (!fileUri.startsWith('gs://')) {
throw new AppError('VALIDATION_ERROR', 'fileUri must start with gs://', 400);
}
@@ -27,6 +27,20 @@ function allowedBuckets() {
]);
}
export function validateFileUriAccess({ fileUri, actorUid }) {
const { bucket, path } = parseGsUri(fileUri);
if (!allowedBuckets().has(bucket)) {
throw new AppError('FORBIDDEN', `Bucket not allowed for signing: ${bucket}`, 403);
}
const ownedPrefix = `uploads/${actorUid}/`;
if (!path.startsWith(ownedPrefix)) {
throw new AppError('FORBIDDEN', 'Cannot sign URL for another user path', 403);
}
return { bucket, path };
}
export async function uploadToGcs({ bucket, objectPath, contentType, buffer }) {
const file = storage.bucket(bucket).file(objectPath);
await file.save(buffer, {
@@ -38,13 +52,14 @@ export async function uploadToGcs({ bucket, objectPath, contentType, buffer }) {
});
}
export async function generateReadSignedUrl({ fileUri, expiresInSeconds }) {
const { bucket, path } = parseGsUri(fileUri);
if (!allowedBuckets().has(bucket)) {
throw new AppError('FORBIDDEN', `Bucket not allowed for signing: ${bucket}`, 403);
export async function generateReadSignedUrl({ fileUri, actorUid, expiresInSeconds }) {
const { bucket, path } = validateFileUriAccess({ fileUri, actorUid });
const file = storage.bucket(bucket).file(path);
const [exists] = await file.exists();
if (!exists) {
throw new AppError('NOT_FOUND', 'File not found for signed URL', 404, { fileUri });
}
const file = storage.bucket(bucket).file(path);
const expiresAtMs = Date.now() + expiresInSeconds * 1000;
const [signedUrl] = await file.getSignedUrl({
version: 'v4',