nexu-io/open-design · error · Error

invalid brand id: ${id}

Error message

invalid brand id: ${id}

What it means

Thrown by createBrandDir when the id fails isValidBrandId. This is the very first write of a brand's lifecycle (mkdir + initial meta.json), so an invalid id here means the brand dir was never created. The id must match /^[a-z0-9][a-z0-9-]*$/ and contain no '..'.

Source

Thrown at apps/daemon/src/brands/store.ts:83

 * Resolve `relParts` under a brand dir with a traversal guard. Returns the
 * absolute path, or null when the id is invalid or the resolved path escapes
 * the brand dir.
 */
export function resolveBrandFile(
  brandsRoot: string,
  id: string,
  relParts: string[],
): string | null {
  if (!isValidBrandId(id)) return null;
  const base = path.resolve(brandDir(brandsRoot, id));
  const target = path.resolve(base, ...relParts);
  if (target !== base && !target.startsWith(`${base}${path.sep}`)) return null;
  return target;
}

/** Create the brand dir and write its initial meta.json. */
export function createBrandDir(brandsRoot: string, id: string, meta: BrandMeta): void {
  if (!isValidBrandId(id)) throw new Error(`invalid brand id: ${id}`);
  fs.mkdirSync(brandDir(brandsRoot, id), { recursive: true });
  writeMeta(brandsRoot, id, meta);
}

function readJson<T>(file: string): T | null {
  try {
    return JSON.parse(fs.readFileSync(file, 'utf8')) as T;
  } catch {
    return null;
  }
}

function writeJson(file: string, value: unknown): void {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  fs.writeFileSync(file, JSON.stringify(value, null, 2), 'utf8');
}

export function readMeta(brandsRoot: string, id: string): BrandMeta | null {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Always mint ids via newBrandId(sourceUrl) (host slug + 6-char random suffix); never accept caller-supplied ids for creation.
  2. At the API boundary, run isValidBrandId(id) and 400 the request on failure.
  3. If you need a deterministic id for tests, use a lowercase-hyphen slug that matches the regex (e.g. 'test-brand-1').
  4. Add a unit test over createBrandDir with a table of invalid ids to lock the contract.

Example fix

// before
createBrandDir(brandsRoot, req.body.id, meta);
// after
import { isValidBrandId, newBrandId } from './store.js';
const id = req.body.id ? validateAndNormalize(req.body.id) : newBrandId(meta.sourceUrl);
if (!isValidBrandId(id)) return res.status(400).json({ error: 'invalid brand id' });
createBrandDir(brandsRoot, id, meta);
Defensive patterns

Strategy: validation

Validate before calling

import { isValidBrandId, newBrandId } from './store.js';
const id = newBrandId(sourceUrl);
if (!isValidBrandId(id)) throw new Error('internal: generated invalid id');

Type guard

import { isValidBrandId } from './store.js';
const isBrandId = (id: unknown): id is string =>
  typeof id === 'string' && isValidBrandId(id);

Prevention

When it happens

Trigger: createBrandDir is invoked with a caller-supplied id that contains uppercase, underscores, slashes, dots, spaces, is empty, or contains '..'. Typically the caller should be using newBrandId(sourceUrl) to mint the id instead.

Common situations: Code path that takes a brand id from request body or CLI arg and passes it straight to createBrandDir; tests that hard-code ids like 'Test_Brand'; a refactor that replaced newBrandId with a hand-built slug; race where the id is computed from untrusted input.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/cd0691bc05c58d34. Report an issue: GitHub.