extract constants

AuthorKonata <konata@posteo.jp>
Date
Commiteae08b6f9b1dbc32ff39993b3b9200b42ad10738
Parent0f72f1e
12 files changed, 78 insertions(+), 27 deletions(-)
Msrc/constants.ts
@@ -26,6 +26,38 @@ export const VALID_KEY_TYPES = new Set([
2626 ]);
2727 export const CHALLENGE_TTL_MS = 5 * 60 * 1000;
2828
29+// Rate limiting
30+export const LOGIN_MAX_ATTEMPTS = 10;
31+export const LOGIN_RATE_WINDOW_MS = 60_000;
32+export const REGISTRATION_MAX_ATTEMPTS = 3;
33+export const REGISTRATION_RATE_WINDOW_MS = 60 * 60_000;
34+
35+// Session
36+export const SESSION_ID_BYTES = 32;
37+export const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000;
38+export const SESSION_DURATION_SECONDS = 30 * 24 * 60 * 60;
39+export const MIN_PASSWORD_LENGTH = 8;
40+
41+// Cookie lifetimes
42+export const YEAR_SECONDS = 365 * 24 * 60 * 60;
43+
44+// File handling
45+export const BINARY_DETECT_BYTES = 8000;
46+
47+// Git ref limits
48+export const MAX_REF_LIST = 1000;
49+
50+// Text preview
51+export const PREVIEW_MAX_LENGTH = 180;
52+export const PREVIEW_TRUNCATION_THRESHOLD = 0.6;
53+
54+// String length limits
55+export const MAX_BRANCH_NAME_LENGTH = 255;
56+export const MAX_TAG_NAME_LENGTH = 255;
57+export const MAX_TAG_MESSAGE_LENGTH = 500;
58+export const MAX_LABEL_NAME_LENGTH = 50;
59+export const MAX_FILE_PATH_LENGTH = 1000;
60+
2961 // Pagination
3062 export const REPOS_PER_PAGE = 20;
3163 export const COMMITS_PER_PAGE = 20;
Msrc/routes/auth.tsx
@@ -10,6 +10,14 @@ import config from "../config.ts";
1010 import {
1111 ADMIN_USERNAME,
1212 CHALLENGE_TTL_MS,
13+ LOGIN_MAX_ATTEMPTS,
14+ LOGIN_RATE_WINDOW_MS,
15+ MIN_PASSWORD_LENGTH,
16+ REGISTRATION_MAX_ATTEMPTS,
17+ REGISTRATION_RATE_WINDOW_MS,
18+ SESSION_DURATION_MS,
19+ SESSION_DURATION_SECONDS,
20+ SESSION_ID_BYTES,
1321 VALID_USERNAME_RE,
1422 WEBAUTHN_RP_NAME,
1523 } from "../constants.ts";
@@ -36,9 +44,9 @@ function randomHex(bytes: number): string {
3644 }
3745
3846 async function createSession(userId: number): Promise<string> {
39- const id = randomHex(32);
47+ const id = randomHex(SESSION_ID_BYTES);
4048 const now = new Date();
41- const expires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
49+ const expires = new Date(now.getTime() + SESSION_DURATION_MS);
4250 await db
4351 .insertInto("sessions")
4452 .values({
@@ -52,7 +60,7 @@ async function createSession(userId: number): Promise<string> {
5260 }
5361
5462 function sessionCookie(id: string): string {
55- return `session=${id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${30 * 24 * 60 * 60}`;
63+ return `session=${id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_DURATION_SECONDS}`;
5664 }
5765
5866 function clearCookie(): string {
@@ -94,7 +102,7 @@ export const authRoutes = new Elysia()
94102 "/login",
95103 async ({ body, request, server }) => {
96104 const ip = getClientIp(request, server);
97- if (!checkRateLimit(ip, 10, 60_000)) {
105+ if (!checkRateLimit(ip, LOGIN_MAX_ATTEMPTS, LOGIN_RATE_WINDOW_MS)) {
98106 return html(
99107 <Login error="Too many login attempts. Please try again later." />,
100108 );
@@ -143,7 +151,7 @@ export const authRoutes = new Elysia()
143151 status: 403,
144152 });
145153 const ip = getClientIp(request, server);
146- if (!checkRateLimit(ip, 3, 60 * 60_000)) {
154+ if (!checkRateLimit(ip, REGISTRATION_MAX_ATTEMPTS, REGISTRATION_RATE_WINDOW_MS)) {
147155 return html(
148156 <Register
149157 error="Too many registration attempts. Please try again later."
@@ -186,7 +194,7 @@ export const authRoutes = new Elysia()
186194 />,
187195 );
188196 }
189- if (password.length < 8) {
197+ if (password.length < MIN_PASSWORD_LENGTH) {
190198 return html(
191199 <Register
192200 error="Password must be at least 8 characters"
Msrc/routes/avatars.ts
@@ -1,5 +1,6 @@
11 import { Elysia, t } from "elysia";
22 import config from "../config.ts";
3+import { YEAR_SECONDS } from "../constants.ts";
34 import { db } from "../db/index.ts";
45 import { requireAuth, resolveSession } from "../middleware/session.ts";
56 import {
@@ -8,7 +9,7 @@ import {
89 processAndStoreAvatar,
910 } from "../services/avatar.ts";
1011
11-const CACHE = "public, max-age=31536000, immutable";
12+const CACHE = `public, max-age=${YEAR_SECONDS}, immutable`;
1213
1314 async function bumpAvatarVersion(userId: number): Promise<void> {
1415 await db
Msrc/routes/repos.tsx
@@ -5,12 +5,16 @@ import { fileTypeFromBuffer } from "file-type";
55 import { sql } from "kysely";
66 import config from "../config.ts";
77 import {
8+ BINARY_DETECT_BYTES,
89 BRANCHES_PER_PAGE,
910 COMMITS_PER_PAGE,
11+ MAX_BRANCH_NAME_LENGTH,
12+ MAX_LABEL_NAME_LENGTH,
1013 paths,
1114 REPOS_PER_PAGE,
1215 TAGS_PER_PAGE,
1316 VALID_REPO_NAME_RE,
17+ YEAR_SECONDS,
1418 } from "../constants.ts";
1519 import { db } from "../db/index.ts";
1620 import { redirect } from "../lib/redirect.ts";
@@ -56,7 +60,7 @@ async function mimeForContent(
5660 return typeFromName;
5761 }
5862
59- return hasBinaryContent(content.subarray(0, 8000))
63+ return hasBinaryContent(content.subarray(0, BINARY_DETECT_BYTES))
6064 ? "application/octet-stream"
6165 : "text/plain; charset=utf-8";
6266 }
@@ -97,7 +101,7 @@ export const repoRoutes = new Elysia()
97101 const sort = body.sort === "name" ? "name" : "created";
98102 return redirect(
99103 "/",
100- `repo_sort=${sort}; Path=/; SameSite=Lax; Max-Age=${365 * 24 * 60 * 60}`,
104+ `repo_sort=${sort}; Path=/; SameSite=Lax; Max-Age=${YEAR_SECONDS}`,
101105 );
102106 },
103107 { body: t.Object({ sort: t.String() }) },
@@ -864,7 +868,7 @@ export const repoRoutes = new Elysia()
864868 const name = body.name?.trim();
865869 const color = body.color?.trim();
866870
867- if (!name || name.length > 50) {
871+ if (!name || name.length > MAX_LABEL_NAME_LENGTH) {
868872 return redirect(
869873 `/${repo.name}/settings?error=${encodeURIComponent("Label name must be 1–50 characters.")}`,
870874 );
@@ -980,7 +984,7 @@ export const repoRoutes = new Elysia()
980984 !name ||
981985 !/^[a-zA-Z0-9._][a-zA-Z0-9._\-/]*$/.test(name) ||
982986 name.includes("..") ||
983- name.length > 255
987+ name.length > MAX_BRANCH_NAME_LENGTH
984988 ) {
985989 return redirect(
986990 `/${repo.name}/branches?error=${encodeURIComponent("Invalid branch name.")}`,
@@ -1077,7 +1081,7 @@ export const repoRoutes = new Elysia()
10771081 !newName ||
10781082 !/^[a-zA-Z0-9._][a-zA-Z0-9._\-/]*$/.test(newName) ||
10791083 newName.includes("..") ||
1080- newName.length > 255
1084+ newName.length > MAX_BRANCH_NAME_LENGTH
10811085 ) {
10821086 return redirect(
10831087 `/${repo.name}/branches?error=${encodeURIComponent("Invalid branch name.")}`,
Msrc/routes/settings.tsx
@@ -6,6 +6,7 @@ import {
66 ADMIN_USERNAME,
77 VALID_KEY_TYPES,
88 VALID_USERNAME_RE,
9+ YEAR_SECONDS,
910 } from "../constants.ts";
1011 import { db } from "../db";
1112 import { redirect } from "../lib/redirect.ts";
@@ -267,7 +268,7 @@ export const settingsRoutes = new Elysia()
267268 return redirect("/settings?error=Invalid+theme");
268269 }
269270
270- const cookieHeader = `theme=${theme}; Path=/; SameSite=Lax; Max-Age=${365 * 24 * 60 * 60}`;
271+ const cookieHeader = `theme=${theme}; Path=/; SameSite=Lax; Max-Age=${YEAR_SECONDS}`;
271272 return redirect("/settings?success=theme", cookieHeader);
272273 },
273274 {
Msrc/services/git.ts
@@ -3,6 +3,7 @@ import { $ as _$ } from "bun";
33
44 import {
55 MAX_BRANCH_CACHE,
6+ MAX_REF_LIST,
67 MAX_TAG_CACHE,
78 paths,
89 REF_CACHE_TTL_MS,
@@ -420,7 +421,7 @@ export const git = {
420421
421422 async branchesWithInfo(
422423 name: string,
423- maxCount = 1000,
424+ maxCount = MAX_REF_LIST,
424425 ): Promise<BranchInfo[]> {
425426 const p = repoPath(name);
426427 try {
Msrc/services/highlight.ts
@@ -8,7 +8,7 @@ import {
88 type Highlighter,
99 } from "shiki";
1010 import config from "../config.ts";
11-import { MAX_FILE_CACHE } from "../constants.ts";
11+import { BINARY_DETECT_BYTES, MAX_FILE_CACHE } from "../constants.ts";
1212
1313 let highlighter: Highlighter | null = null;
1414 let extToLangId: Map<string, string> | null = null;
@@ -72,7 +72,7 @@ export function getHighlighter(): Highlighter {
7272 }
7373
7474 export function hasBinaryContent(buf: Buffer): boolean {
75- return buf.subarray(0, 8000).includes(0);
75+ return buf.subarray(0, BINARY_DETECT_BYTES).includes(0);
7676 }
7777
7878 export function detectLang(filename: string): string {
Msrc/services/markdown.ts
@@ -1,6 +1,6 @@
11 import DOMPurify from "isomorphic-dompurify";
22 import { Marked, marked, type Tokens } from "marked";
3-import { MAX_MD_CACHE } from "../constants.ts";
3+import { MAX_MD_CACHE, PREVIEW_MAX_LENGTH, PREVIEW_TRUNCATION_THRESHOLD } from "../constants.ts";
44
55 marked.setOptions({ gfm: true });
66
@@ -181,14 +181,14 @@ export function markdownToPlaintext(md: string): string {
181181 * Returns a short single-line preview of a plaintext string:
182182 * the first paragraph/heading line, truncated to maxLen chars.
183183 */
184-export function plaintextPreview(text: string, maxLen = 180): string {
184+export function plaintextPreview(text: string, maxLen = PREVIEW_MAX_LENGTH): string {
185185 const firstBlock = text.split("\n\n")[0]?.trim() ?? "";
186186 const firstLine = firstBlock.split("\n")[0] ?? "";
187187 if (firstLine.length <= maxLen) return firstLine;
188188 const truncated = firstLine.slice(0, maxLen);
189189 const lastSpace = truncated.lastIndexOf(" ");
190190 return (
191- (lastSpace > maxLen * 0.6 ? truncated.slice(0, lastSpace) : truncated) +
191+ (lastSpace > maxLen * PREVIEW_TRUNCATION_THRESHOLD ? truncated.slice(0, lastSpace) : truncated) +
192192 "…"
193193 );
194194 }
Msrc/views/repos/BranchList.tsx
@@ -2,6 +2,7 @@ import type { RepositoryRow } from "../../db/index.ts";
22 import { formatDateTime } from "../../lib/formatDate.ts";
33 import type { SessionUser } from "../../middleware/session.ts";
44 import type { BranchInfo } from "../../services/git.ts";
5+import { MAX_BRANCH_NAME_LENGTH } from "../../constants.ts";
56 import { Layout } from "../layout.tsx";
67 import { Pagination } from "../Pagination.tsx";
78 import { RepoHeader } from "./RepoHeader.tsx";
@@ -56,7 +57,7 @@ export function BranchList({
5657 type="text"
5758 required
5859 placeholder="feature/my-branch"
59- maxlength="255"
60+ maxlength={MAX_BRANCH_NAME_LENGTH}
6061 />
6162 </div>
6263 <div class="form-group">
@@ -70,7 +71,7 @@ export function BranchList({
7071 required
7172 value={repo.default_branch}
7273 placeholder="branch, tag, or commit"
73- maxlength="255"
74+ maxlength={MAX_BRANCH_NAME_LENGTH}
7475 />
7576 </div>
7677 <button
@@ -153,7 +154,7 @@ export function BranchList({
153154 required
154155 placeholder="new-name"
155156 value={b.name}
156- maxlength="255"
157+ maxlength={MAX_BRANCH_NAME_LENGTH}
157158 />
158159 <button
159160 type="submit"
Msrc/views/repos/FileEdit.tsx
@@ -1,5 +1,6 @@
11 import type { RepositoryRow } from "../../db/index.ts";
22 import type { SessionUser } from "../../middleware/session.ts";
3+import { MAX_FILE_PATH_LENGTH } from "../../constants.ts";
34 import { Layout } from "../layout.tsx";
45 import { RepoHeader } from "../repos/RepoHeader.tsx";
56 import { RepoNav } from "./RepoNav.tsx";
@@ -102,7 +103,7 @@ export function FileEdit({
102103 type="text"
103104 value={filePath}
104105 class="mono"
105- maxlength="1000"
106+ maxlength={MAX_FILE_PATH_LENGTH}
106107 />
107108 </div>
108109 <div class="form-group">
Msrc/views/repos/NewFileForm.tsx
@@ -1,5 +1,6 @@
11 import type { RepositoryRow } from "../../db/index.ts";
22 import type { SessionUser } from "../../middleware/session.ts";
3+import { MAX_FILE_PATH_LENGTH } from "../../constants.ts";
34 import { Layout } from "../layout.tsx";
45 import { RepoHeader } from "../repos/RepoHeader.tsx";
56 import { RepoNav } from "./RepoNav.tsx";
@@ -70,7 +71,7 @@ export function NewFileForm({
7071 value={defaultPath}
7172 placeholder="path/to/file.txt"
7273 class="mono"
73- maxlength="1000"
74+ maxlength={MAX_FILE_PATH_LENGTH}
7475 />
7576 </div>
7677 <div class="form-group">
Msrc/views/repos/TagList.tsx
@@ -2,6 +2,7 @@ import type { RepositoryRow } from "../../db/index.ts";
22 import { formatDateTime } from "../../lib/formatDate.ts";
33 import type { SessionUser } from "../../middleware/session.ts";
44 import type { TagInfo } from "../../services/git.ts";
5+import { MAX_TAG_MESSAGE_LENGTH, MAX_TAG_NAME_LENGTH } from "../../constants.ts";
56 import { Layout } from "../layout.tsx";
67 import { Pagination } from "../Pagination.tsx";
78 import { RepoHeader } from "./RepoHeader.tsx";
@@ -58,7 +59,7 @@ export function TagList({
5859 type="text"
5960 required
6061 placeholder="v1.0.0"
61- maxlength="255"
62+ maxlength={MAX_TAG_NAME_LENGTH}
6263 />
6364 </div>
6465 <div class="form-group">
@@ -70,7 +71,7 @@ export function TagList({
7071 required
7172 value={repo.default_branch}
7273 placeholder="branch, tag, or commit"
73- maxlength="255"
74+ maxlength={MAX_TAG_NAME_LENGTH}
7475 />
7576 </div>
7677 <div class="form-group">
@@ -85,7 +86,7 @@ export function TagList({
8586 name="message"
8687 type="text"
8788 placeholder="Optional tag message"
88- maxlength="500"
89+ maxlength={MAX_TAG_MESSAGE_LENGTH}
8990 />
9091 </div>
9192 <button