nexu-io/open-design · error · Error

invalid design system id: ${designSystemId}

Error message

invalid design system id: ${designSystemId}

What it means

Thrown by syncBrandSystemToUserDesignSystem when userDesignSystemDir(root, designSystemId) returns null. The id must start with the literal prefix 'user:', the slug after the prefix must match /^[a-z0-9][a-z0-9-]*$/ (lowercase alphanumeric + hyphens, starting alphanumeric), AND the resolved path must stay inside root (path-containment guard). Built-in ids without the 'user:' prefix are rejected here.

Source

Thrown at apps/daemon/src/brands/index.ts:2166

  metadata: ProjectMetadata,
  sourceDir: string,
  targetPrefix: string,
): Promise<void> {
  for (const file of collectFiles(sourceDir)) {
    const projectPath = toPosixPath(path.join(targetPrefix, file.rel));
    await writeProjectFile(projectsRoot, projectId, projectPath, fs.readFileSync(file.abs), { overwrite: true }, metadata);
  }
}

function syncBrandSystemToUserDesignSystem(
  userDesignSystemsRoot: string,
  designSystemId: string,
  brandsRoot: string,
  brandId: string,
  designMd: string,
): void {
  const dir = userDesignSystemDir(userDesignSystemsRoot, designSystemId);
  if (!dir) throw new Error(`invalid design system id: ${designSystemId}`);
  const brandRoot = resolveBrandFile(brandsRoot, brandId, []);
  if (!brandRoot) throw new Error(`invalid brand id: ${brandId}`);

  fs.writeFileSync(path.join(dir, 'DESIGN.md'), designMd, 'utf8');
  copyDirectorySync(brandSystemDir(brandsRoot, brandId), path.join(dir, 'system'));
  copyOptionalDirectorySync(path.join(brandRoot, 'logos'), path.join(dir, 'logos'));
  copyOptionalDirectorySync(path.join(brandRoot, 'fonts'), path.join(dir, 'fonts'));
  copyOptionalDirectorySync(path.join(brandRoot, 'imagery'), path.join(dir, 'imagery'));
  copyOptionalDirectorySync(path.join(brandRoot, 'prefetch'), path.join(dir, 'prefetch'));
  const brandJson = resolveBrandFile(brandsRoot, brandId, ['brand.json']);
  if (brandJson && isFile(brandJson)) {
    fs.copyFileSync(brandJson, path.join(dir, 'brand.json'));
  }
}

function userDesignSystemDir(root: string, id: string): string | null {
  if (!id.startsWith('user:')) return null;
  const dirId = id.slice('user:'.length);

View on GitHub (pinned to 5be4028344)

Solutions

  1. Prefix the id with 'user:' (e.g. 'user:my-brand').
  2. Ensure the slug after the prefix is lowercase, starts with an alphanumeric, and uses only [a-z0-9-].
  3. Reject ids with '/', '\', '..', or uppercase before calling this function.

Example fix

// before
syncBrandSystemToUserDesignSystem(root, 'My-Brand', brandsRoot, brandId, designMd); // missing user: prefix + uppercase -> throws

// after
syncBrandSystemToUserDesignSystem(root, 'user:my-brand', brandsRoot, brandId, designMd);
Defensive patterns

Strategy: validation

Validate before calling

function isUserDesignSystemId(id: string): boolean {
  if (!id.startsWith('user:')) return false;
  const slug = id.slice('user:'.length);
  return /^[a-z0-9][a-z0-9-]*$/u.test(slug);
}
if (!isUserDesignSystemId(designSystemId)) {
  throw new Error(`Design system id must start with 'user:' and use a lowercase kebab slug.`);
}

Type guard

function isUserDesignSystemId(id: unknown): id is string {
  return typeof id === 'string'
    && id.startsWith('user:')
    && /^[a-z0-9][a-z0-9-]*$/u.test(id.slice('user:'.length));
}

Try / catch

try {
  syncBrandSystemToUserDesignSystem(root, designSystemId, brandsRoot, brandId, designMd);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('invalid design system id:')) {
    return badRequest("Design system id must be 'user:<lowercase-kebab-slug>'.");
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing a design-system id that omits the 'user:' prefix (e.g. a built-in id); an id with uppercase, underscores, dots, or slashes; an id whose resolved path escapes the user design systems root.

Common situations: Using a raw slug instead of the prefixed form; passing a built-in design-system id where a user one is required; constructing ids from untrusted input without the prefix or charset check.

Related errors


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