trails/packages/api/src/errors.ts
Ullrich Schäfer 067e2ebd0b
fix: enforce Terms gate on bearer-token API requests
Mobile API requests authenticated via OAuth2 bearer tokens bypassed the
Terms gate that the root loader applies to web cookie sessions. Extend
requireApiUser to compare the user's termsVersion with TERMS_VERSION
and return a structured 403 { code: "TERMS_OUTDATED", currentTermsVersion }
on mismatch so mobile clients can surface their own re-acceptance UI.

Spec delta on journal-auth captures the new requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:59:28 +02:00

39 lines
1.1 KiB
TypeScript

import { z } from "zod";
export const FieldErrorSchema = z.object({
field: z.string(),
message: z.string(),
});
export const ApiErrorResponseSchema = z.object({
error: z.string(),
code: z.string(),
fields: z.array(FieldErrorSchema).optional(),
});
export type FieldError = z.infer<typeof FieldErrorSchema>;
export type ApiErrorResponse = z.infer<typeof ApiErrorResponseSchema>;
/**
* Map a ZodError's issues to the FieldError[] shape used in API
* validation responses. Keeps the path-flattening rule in one place so
* every route returns validation errors in the same shape.
*/
export function zodIssuesToFieldErrors(error: z.ZodError): FieldError[] {
return error.issues.map((i) => ({
field: i.path.join("."),
message: i.message,
}));
}
/** Standard error codes */
export const ERROR_CODES = {
VALIDATION_ERROR: "VALIDATION_ERROR",
NOT_FOUND: "NOT_FOUND",
UNAUTHORIZED: "UNAUTHORIZED",
FORBIDDEN: "FORBIDDEN",
CONFLICT: "CONFLICT",
RATE_LIMITED: "RATE_LIMITED",
INTERNAL_ERROR: "INTERNAL_ERROR",
TERMS_OUTDATED: "TERMS_OUTDATED",
} as const;