Compare commits

..

3 Commits

Author SHA1 Message Date
Mauricio Siu
7ca96f6087 fix(editor): consistent YAML auto-indent on newline
The indentNodeProp from @codemirror/lang-yaml computes column-based
indents that produce inconsistent and odd-numbered indentation when
pressing Enter (e.g. 9 spaces after a nested 'web:' line, 6 after a
4-space 'image:' line). Override it with a line-based indentService:
keep the current line's indent, align list-entry keys after the dash
marker, and indent one unit deeper after lines opening a block
(':', '|', '>').

Fixes #4650
2026-07-14 11:57:12 -06:00
Mauricio Siu
7ba9818894 Merge pull request #4806 from tanaymishra/fix/validate-api-key-name-length
fix: validate API key name length to prevent opaque 500
2026-07-14 00:08:04 -06:00
tanaymishra
f577778667 fix: validate API key name length to prevent opaque 500
API key names longer than 32 characters were rejected by better-auth
with a 400 that surfaced as an opaque INTERNAL_SERVER_ERROR (the generic
"Failed to generate API key" toast). Add a shared name schema (min 1,
max 32, matching better-auth's default maximumNameLength) used by both
the tRPC input and the client form, and surface the limit on the Name
field so users see it before submitting.

Fixes #4798
2026-07-12 18:15:05 +05:30
5 changed files with 88 additions and 5 deletions

View File

@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
describe("apiKeyNameSchema", () => {
it("rejects an empty name", () => {
const result = apiKeyNameSchema.safeParse("");
expect(result.success).toBe(false);
});
it("accepts a name at the maximum length", () => {
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH);
const result = apiKeyNameSchema.safeParse(name);
expect(result.success).toBe(true);
});
it("rejects a name over the maximum length instead of passing it to better-auth", () => {
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH + 1);
const result = apiKeyNameSchema.safeParse(name);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toBe(
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
);
}
});
});

View File

@@ -32,10 +32,11 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
import { api } from "@/utils/api";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
name: apiKeyNameSchema,
prefix: z.string().optional(),
expiresIn: z.number().nullable(),
organizationId: z.string().min(1, "Organization is required"),
@@ -159,8 +160,15 @@ export const AddApiKey = () => {
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="My API Key" {...field} />
<Input
placeholder="My API Key"
maxLength={API_KEY_NAME_MAX_LENGTH}
{...field}
/>
</FormControl>
<FormDescription>
Maximum {API_KEY_NAME_MAX_LENGTH} characters
</FormDescription>
<FormMessage />
</FormItem>
)}

View File

@@ -7,7 +7,11 @@ import {
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import {
getIndentUnit,
indentService,
StreamLanguage,
} from "@codemirror/language";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { search, searchKeymap } from "@codemirror/search";
@@ -98,6 +102,27 @@ const dockerComposeServiceOptions = [
},
}));
// The indentNodeProp shipped with @codemirror/lang-yaml computes wrong
// column-based indents on Enter (odd/inconsistent amounts, see #4650), so
// indentation is resolved line-based here: keep the current indent, align
// list-entry keys after the dash marker, and go one unit deeper after a
// line that opens a block (ending in ":", "|" or ">").
const yamlIndent = indentService.of((context, pos) => {
const line = context.state.doc.lineAt(pos);
const before = context.state.doc.sliceString(line.from, pos);
const indent = /^ */.exec(before)?.[0].length ?? 0;
const trimmed = before.trim();
if (!trimmed || trimmed.startsWith("#")) {
return indent;
}
const base =
trimmed.startsWith("- ") && /:\s/.test(trimmed) ? indent + 2 : indent;
if (/[:|>]$/.test(trimmed)) {
return base + getIndentUnit(context.state);
}
return base;
});
function dockerComposeComplete(
context: CompletionContext,
): CompletionResult | null {
@@ -160,7 +185,7 @@ export const CodeEditor = ({
search(),
keymap.of(searchKeymap),
language === "yaml"
? yaml()
? [yamlIndent, yaml()]
: language === "json"
? json()
: language === "css"

View File

@@ -0,0 +1,23 @@
import { z } from "zod";
/**
* Maximum length allowed for an API key name.
*
* This mirrors the default `maximumNameLength` enforced by the
* `@better-auth/api-key` plugin. Names longer than this are rejected by
* better-auth with a 400, so we validate against it up front to surface a
* clear field-level error instead of an opaque 500.
*/
export const API_KEY_NAME_MAX_LENGTH = 32;
/**
* Shared validation for an API key name, used by both the tRPC input schema
* and the client form so the two can't drift.
*/
export const apiKeyNameSchema = z
.string()
.min(1, "Name is required")
.max(
API_KEY_NAME_MAX_LENGTH,
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
);

View File

@@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { and, asc, eq, gt, ne } from "drizzle-orm";
import { z } from "zod";
import { apiKeyNameSchema } from "@/lib/api-keys";
import { audit } from "@/server/api/utils/audit";
import {
adminProcedure,
@@ -45,7 +46,7 @@ import {
} from "../trpc";
const apiCreateApiKey = z.object({
name: z.string().min(1),
name: apiKeyNameSchema,
prefix: z.string().optional(),
expiresIn: z.number().optional(),
metadata: z.object({