From e7dd220106d86b503434ca1364b373aac24e7a7e Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 4 Mar 2026 19:25:51 -0800 Subject: [PATCH 1/3] Allow dots in app and repo names --- apps/git/index.tsx | 2 +- src/client/modals/NewApp.tsx | 4 ++-- src/client/modals/RenameApp.tsx | 4 ++-- src/server/api/apps.ts | 4 ++-- src/server/apps.ts | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/git/index.tsx b/apps/git/index.tsx index 571ec03..962a4b0 100644 --- a/apps/git/index.tsx +++ b/apps/git/index.tsx @@ -13,7 +13,7 @@ const DATA_ROOT = process.env.DATA_ROOT! const TOES_URL = process.env.TOES_URL! const REPOS_DIR = resolve(DATA_ROOT, 'repos') -const VALID_NAME = /^[a-zA-Z0-9_-]+$/ +const VALID_NAME = /^[a-zA-Z0-9._-]+$/ const VISIBILITY_PATH = join(DATA_DIR, 'visibility.json') const TOGGLE_SCRIPT = ` diff --git a/src/client/modals/NewApp.tsx b/src/client/modals/NewApp.tsx index e48e654..f39ce5b 100644 --- a/src/client/modals/NewApp.tsx +++ b/src/client/modals/NewApp.tsx @@ -20,8 +20,8 @@ async function createNewApp() { return } - if (!/^[a-z][a-z0-9-]*$/.test(name)) { - newAppError = 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens' + if (!/^[a-z][a-z0-9.-]*$/.test(name)) { + newAppError = 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' renderModal() return } diff --git a/src/client/modals/RenameApp.tsx b/src/client/modals/RenameApp.tsx index 3e8c2ca..001e0f2 100644 --- a/src/client/modals/RenameApp.tsx +++ b/src/client/modals/RenameApp.tsx @@ -19,8 +19,8 @@ async function doRenameApp(input: HTMLInputElement) { return } - if (!/^[a-z][a-z0-9-]*$/.test(newName)) { - renameAppError = 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens' + if (!/^[a-z][a-z0-9.-]*$/.test(newName)) { + renameAppError = 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' renderModal() return } diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts index 06d94a0..9b48081 100644 --- a/src/server/api/apps.ts +++ b/src/server/api/apps.ts @@ -120,8 +120,8 @@ router.post('/', async c => { const name = body.name?.trim().toLowerCase().replace(/\s+/g, '-') if (!name) return c.json({ ok: false, error: 'App name is required' }, 400) - if (!/^[a-z][a-z0-9-]*$/.test(name)) { - return c.json({ ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens' }, 400) + if (!/^[a-z][a-z0-9.-]*$/.test(name)) { + return c.json({ ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' }, 400) } const appPath = join(APPS_DIR, name) diff --git a/src/server/apps.ts b/src/server/apps.ts index b8a4c8e..3751745 100644 --- a/src/server/apps.ts +++ b/src/server/apps.ts @@ -176,8 +176,8 @@ export async function renameApp(oldName: string, newName: string): Promise<{ ok: if (_apps.has(newName)) return { ok: false, error: 'An app with that name already exists' } - if (!/^[a-z][a-z0-9-]*$/.test(newName)) { - return { ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens' } + if (!/^[a-z][a-z0-9.-]*$/.test(newName)) { + return { ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' } } const oldPath = join(APPS_DIR, oldName) From 0af360cef2b8d307060141e50366f5823a7821e6 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 5 Mar 2026 07:45:19 -0800 Subject: [PATCH 2/3] Centralize VALID_NAME regex into shared types --- apps/git/index.tsx | 3 +-- src/client/modals/NewApp.tsx | 3 ++- src/client/modals/RenameApp.tsx | 3 ++- src/server/api/apps.ts | 3 ++- src/server/apps.ts | 4 ++-- src/shared/types.ts | 1 + src/tools/index.ts | 1 + 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/git/index.tsx b/apps/git/index.tsx index 962a4b0..f8dfdc4 100644 --- a/apps/git/index.tsx +++ b/apps/git/index.tsx @@ -1,6 +1,6 @@ import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' -import { baseStyles, ToolScript, theme, on } from '@because/toes/tools' +import { baseStyles, ToolScript, theme, on, VALID_NAME } from '@because/toes/tools' import { mkdirSync } from 'fs' import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promises' import { join, resolve } from 'path' @@ -13,7 +13,6 @@ const DATA_ROOT = process.env.DATA_ROOT! const TOES_URL = process.env.TOES_URL! const REPOS_DIR = resolve(DATA_ROOT, 'repos') -const VALID_NAME = /^[a-zA-Z0-9._-]+$/ const VISIBILITY_PATH = join(DATA_DIR, 'visibility.json') const TOGGLE_SCRIPT = ` diff --git a/src/client/modals/NewApp.tsx b/src/client/modals/NewApp.tsx index f39ce5b..95850f5 100644 --- a/src/client/modals/NewApp.tsx +++ b/src/client/modals/NewApp.tsx @@ -1,3 +1,4 @@ +import { VALID_NAME } from '../../shared/types' import { closeModal, openModal, renderModal } from '../components/modal' import { navigate } from '../router' import { apps } from '../state' @@ -20,7 +21,7 @@ async function createNewApp() { return } - if (!/^[a-z][a-z0-9.-]*$/.test(name)) { + if (!VALID_NAME.test(name)) { newAppError = 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' renderModal() return diff --git a/src/client/modals/RenameApp.tsx b/src/client/modals/RenameApp.tsx index 001e0f2..8e452a2 100644 --- a/src/client/modals/RenameApp.tsx +++ b/src/client/modals/RenameApp.tsx @@ -1,4 +1,5 @@ import type { App } from '../../shared/types' +import { VALID_NAME } from '../../shared/types' import { closeModal, openModal, renderModal } from '../components/modal' import { navigate } from '../router' import { apps } from '../state' @@ -19,7 +20,7 @@ async function doRenameApp(input: HTMLInputElement) { return } - if (!/^[a-z][a-z0-9.-]*$/.test(newName)) { + if (!VALID_NAME.test(newName)) { renameAppError = 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' renderModal() return diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts index 9b48081..f8bd9b7 100644 --- a/src/server/api/apps.ts +++ b/src/server/api/apps.ts @@ -3,6 +3,7 @@ import { buildAppUrl } from '@urls' import { isTunnelsAvailable, shareApp, unshareApp } from '../tunnels' import type { App as BackendApp } from '$apps' import type { App as SharedApp } from '@types' +import { VALID_NAME } from '@types' import { generateTemplates, type TemplateType } from '%templates' import { Hype } from '@because/hype' import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' @@ -120,7 +121,7 @@ router.post('/', async c => { const name = body.name?.trim().toLowerCase().replace(/\s+/g, '-') if (!name) return c.json({ ok: false, error: 'App name is required' }, 400) - if (!/^[a-z][a-z0-9.-]*$/.test(name)) { + if (!VALID_NAME.test(name)) { return c.json({ ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' }, 400) } diff --git a/src/server/apps.ts b/src/server/apps.ts index 3751745..1382c46 100644 --- a/src/server/apps.ts +++ b/src/server/apps.ts @@ -1,7 +1,7 @@ import type { App as SharedApp, AppState } from '@types' import type { ToesEvent, ToesEventInput, ToesEventType } from '../shared/events' import type { Subprocess } from 'bun' -import { DEFAULT_EMOJI } from '@types' +import { DEFAULT_EMOJI, VALID_NAME } from '@types' import { buildAppUrl, toSubdomain } from '@urls' import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs' import { LOCAL_HOST } from '%config' @@ -176,7 +176,7 @@ export async function renameApp(oldName: string, newName: string): Promise<{ ok: if (_apps.has(newName)) return { ok: false, error: 'An app with that name already exists' } - if (!/^[a-z][a-z0-9.-]*$/.test(newName)) { + if (!VALID_NAME.test(newName)) { return { ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, dots, and hyphens' } } diff --git a/src/shared/types.ts b/src/shared/types.ts index 64984da..38c1ce4 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,4 +1,5 @@ export const DEFAULT_EMOJI = '🖥️' +export const VALID_NAME = /^[a-z][a-z0-9.-]*$/ export interface FileInfo { hash: string diff --git a/src/tools/index.ts b/src/tools/index.ts index d2a98e2..d5b1cdb 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,3 +1,4 @@ +export { VALID_NAME } from '../shared/types' export { theme } from '../client/themes' export { loadAppEnv } from './env' export type { ToesEvent, ToesEventType } from './events' From fafef70a33b4447504040917a5ed94e7198b784a Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 5 Mar 2026 07:46:07 -0800 Subject: [PATCH 3/3] Allow uppercase letters in VALID_NAME regex --- src/shared/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/types.ts b/src/shared/types.ts index 38c1ce4..5438fa6 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,5 +1,5 @@ export const DEFAULT_EMOJI = '🖥️' -export const VALID_NAME = /^[a-z][a-z0-9.-]*$/ +export const VALID_NAME = /^[a-zA-Z][a-zA-Z0-9.-]*$/ export interface FileInfo { hash: string