ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { Ratelimit } from '@upstash/ratelimit'
|
|
import { Redis } from '@upstash/redis'
|
|
import { NextResponse } from 'next/server'
|
|
|
|
let redis: Redis | null = null
|
|
|
|
function getRedis(): Redis | null {
|
|
if (redis) return redis
|
|
const url = process.env.UPSTASH_REDIS_REST_URL
|
|
const token = process.env.UPSTASH_REDIS_REST_TOKEN
|
|
if (!url || !token) return null
|
|
redis = new Redis({ url, token })
|
|
return redis
|
|
}
|
|
|
|
const limiters = new Map<string, Ratelimit>()
|
|
|
|
function getLimiter(prefix: string, maxRequests: number, windowMs: number): Ratelimit | null {
|
|
const key = `${prefix}:${maxRequests}:${windowMs}`
|
|
const cached = limiters.get(key)
|
|
if (cached) return cached
|
|
|
|
const client = getRedis()
|
|
if (!client) return null
|
|
|
|
const limiter = new Ratelimit({
|
|
redis: client,
|
|
limiter: Ratelimit.slidingWindow(maxRequests, `${windowMs} ms`),
|
|
prefix,
|
|
analytics: false,
|
|
})
|
|
limiters.set(key, limiter)
|
|
return limiter
|
|
}
|
|
|
|
export interface RateLimitOptions {
|
|
prefix: string
|
|
identifier: string
|
|
maxRequests: number
|
|
windowMs: number
|
|
}
|
|
|
|
export interface RateLimitResult {
|
|
ok: boolean
|
|
response?: NextResponse
|
|
}
|
|
|
|
/**
|
|
* HTTP rate limit check using Upstash Ratelimit (sliding window).
|
|
*
|
|
* Returns `{ ok: true }` when the request is allowed.
|
|
* Returns `{ ok: false, response }` with a 429 NextResponse when blocked.
|
|
*
|
|
* No-ops (allows the request) when Upstash env vars are not configured:
|
|
* intentional so local dev and self-hosted deployments without Redis still work.
|
|
* Production hosted deployments must set UPSTASH_REDIS_REST_URL/TOKEN for the
|
|
* limit to be enforced; absence is logged once at startup by other call sites.
|
|
*/
|
|
export async function checkRateLimit(opts: RateLimitOptions): Promise<RateLimitResult> {
|
|
const limiter = getLimiter(opts.prefix, opts.maxRequests, opts.windowMs)
|
|
if (!limiter) return { ok: true }
|
|
|
|
const { success, reset, limit, remaining } = await limiter.limit(opts.identifier)
|
|
if (success) return { ok: true }
|
|
|
|
const retryAfterSec = Math.max(1, Math.ceil((reset - Date.now()) / 1000))
|
|
const response = NextResponse.json(
|
|
{ error: 'För många förfrågningar. Försök igen om en stund.' },
|
|
{ status: 429 }
|
|
)
|
|
response.headers.set('Retry-After', String(retryAfterSec))
|
|
response.headers.set('X-RateLimit-Limit', String(limit))
|
|
response.headers.set('X-RateLimit-Remaining', String(remaining))
|
|
response.headers.set('X-RateLimit-Reset', String(Math.ceil(reset / 1000)))
|
|
return { ok: false, response }
|
|
}
|