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>
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { createLogger } from '@/lib/logger'
|
|
import { checkRateLimit } from '@/lib/auth/rate-limit-http'
|
|
import { truncateIp } from '@/lib/api/v1/with-api-v1'
|
|
|
|
const log = createLogger('onboarding-client')
|
|
|
|
// This is an UNAUTHENTICATED client telemetry sink: it's called from the
|
|
// browser during onboarding, before a session necessarily exists, so it can't
|
|
// require auth. Abuse is bounded instead by a per-/24 rate limit (log-flooding,
|
|
// SOC2 CC6.1) and size caps on the client-supplied fields (OWASP V2.2). PII in
|
|
// message/extra is redacted by the structured logger before it reaches Vercel.
|
|
const RATE_LIMIT = { maxRequests: 30, windowMs: 60_000 }
|
|
const MAX_MESSAGE_LEN = 2000
|
|
const MAX_EXTRA_BYTES = 8000
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const fwd = request.headers.get('x-forwarded-for')
|
|
const rawIp = fwd ? fwd.split(',')[0]?.trim() : request.headers.get('x-real-ip') ?? undefined
|
|
const identifier = truncateIp(rawIp || undefined) ?? 'unknown'
|
|
const rl = await checkRateLimit({ prefix: 'client-log', identifier, ...RATE_LIMIT })
|
|
if (!rl.ok) return rl.response!
|
|
|
|
const body = await request.json()
|
|
|
|
// Bound the client-supplied fields before logging: cap the message length
|
|
// and the serialized size of `extra` so a single request can't flood the
|
|
// log pipeline. Shape is coerced rather than strictly schema-rejected:
|
|
// dropping a malformed error report would lose the telemetry this endpoint
|
|
// exists to capture.
|
|
const message =
|
|
typeof body?.message === 'string' ? body.message.slice(0, MAX_MESSAGE_LEN) : 'client onboarding error'
|
|
let extra = body?.extra
|
|
if (extra !== undefined) {
|
|
try {
|
|
if (JSON.stringify(extra).length > MAX_EXTRA_BYTES) extra = { truncated: true }
|
|
} catch {
|
|
extra = undefined // non-serializable (e.g. cyclic), drop it
|
|
}
|
|
}
|
|
|
|
// Route through the structured logger so message + extra are PII-redacted
|
|
// (personnummer / IBAN / tokens via REDACT_KEYS) before reaching Vercel logs.
|
|
log.error('client onboarding error', { clientMessage: message, extra })
|
|
|
|
return NextResponse.json({ ok: true })
|
|
} catch {
|
|
return NextResponse.json({ ok: false }, { status: 400 })
|
|
}
|
|
}
|