{"record":{"id":"72a0bbbf714bd784","repo":"can1357/oh-my-pi","slug":"invalid-global-daemon-service-name-json-stringi","errorCode":null,"errorMessage":"Invalid global daemon service name: ${JSON.stringify(service)}","messagePattern":"Invalid global daemon service name: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/utils/src/dirs.ts","lineNumber":971,"sourceCode":"export function getDaemonRuntimeRoot(): string {\n\treturn dirs.rootSubdir(path.join(\"run\", \"daemons\"), \"state\");\n}\n\n/** Get the daemon runtime directory for a project (~/.omp/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/omp/run/daemons/<hash>). */\nexport function getDaemonRuntimeDir(projectDir: string): string {\n\tconst key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, \"0\");\n\treturn path.join(getDaemonRuntimeRoot(), key);\n}\n\n/** Root directory containing every machine-global daemon service scope. */\nexport function getGlobalDaemonRuntimeRoot(): string {\n\treturn path.join(getBaseConfigRoot(), \"run\", \"daemons\", \"global\");\n}\n\n/** Get a profile-independent runtime directory for a machine-global daemon service. */\nexport function getGlobalDaemonRuntimeDir(service: string): string {\n\tif (!/^[a-z0-9][a-z0-9._-]*$/i.test(service)) {\n\t\tthrow new Error(`Invalid global daemon service name: ${JSON.stringify(service)}`);\n\t}\n\treturn path.join(getGlobalDaemonRuntimeRoot(), service);\n}\n\n/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */\nexport function getProviderInFlightRoot(): string {\n\treturn dirs.rootSubdir(path.join(\"run\", \"provider-inflight\"), \"state\");\n}\n\n/** Get the marketplaces registry path (~/.omp/marketplaces.json; XDG default: $XDG_DATA_HOME/omp/marketplaces.json). Adopts a legacy registry on first XDG resolution. */\nexport function getMarketplacesRegistryPath(): string {\n\tconst registryPath = dirs.rootSubdir(\"marketplaces.json\", \"data\");\n\tadoptLegacyFile(path.join(dirs.configRoot, \"marketplaces.json\"), registryPath);\n\treturn registryPath;\n}\n\n// =============================================================================\n// Project subdirectories (.omp/*)","sourceCodeStart":953,"sourceCodeEnd":989,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/dirs.ts#L953-L989","documentation":"getGlobalDaemonRuntimeDir(service) builds a machine-global runtime directory path by joining the service name under the daemon runtime root. Because the name becomes a filesystem path segment, it is validated against /^[a-z0-9][a-z0-9._-]*$/i; any name containing path separators, leading dots, spaces, or other special characters throws this error to prevent path traversal or malformed runtime paths.","triggerScenarios":"Calling getGlobalDaemonRuntimeDir() with a service string that is empty, starts with a dot or non-alphanumeric character, or contains characters outside [a-z0-9._-] — e.g. \"my daemon\", \"svc/v2\", \"../escape\", \"-svc\", \"\".","commonSituations":"Passing a user-supplied profile/project name straight through as the daemon service name; building the service id by concatenating strings with slashes or colons (e.g. \"org:tool\"); interpolating an env value or config key that contains spaces or dots at the start.","solutions":["Sanitize the service name before calling: strip or replace illegal characters, e.g. name.replace(/[^a-zA-Z0-9._-]/g, \"-\").","Ensure the name starts with an alphanumeric character (prepend one or trim leading dots/dashes).","Derive the service name from a fixed, known identifier (package/tool name) rather than free-form user input.","Validate with the same regex the library uses ( /^[a-z0-9][a-z0-9._-]*$/i ) at your config-loading boundary and reject invalid config early."],"exampleFix":"// before\nconst dir = getGlobalDaemonRuntimeDir(`${org}/${tool}`); // \"acme/preview\" throws\n\n// after\nconst service = `${org}-${tool}`.replace(/[^a-zA-Z0-9._-]/g, \"-\").replace(/^[^a-zA-Z0-9]+/, \"\");\nconst dir = getGlobalDaemonRuntimeDir(service);","handlingStrategy":"validation","validationCode":"const SERVICE_RE = /^[a-z0-9][a-z0-9._-]*$/i;\nfunction isValidDaemonService(name: string): boolean {\n  return SERVICE_RE.test(name);\n}\nif (!isValidDaemonService(service)) throw new Error(`Refusing invalid daemon service name: ${service}`);","typeGuard":"function isValidDaemonService(name: string): boolean {\n  return /^[a-z0-9][a-z0-9._-]*$/i.test(name);\n}","tryCatchPattern":"let dir: string;\ntry {\n  dir = getGlobalDaemonRuntimeDir(service);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Invalid global daemon service name\")) {\n    dir = getGlobalDaemonRuntimeDir(sanitizeServiceName(service));\n  } else throw err;\n}","preventionTips":["Sanitize user/config-derived names with .replace(/[^a-zA-Z0-9._-]/g, \"-\") before use.","Never pass strings containing \"/\", \"\\\\\", \":\", or spaces as path segments.","Validate identifiers at the config-parsing boundary, not at the call site.","Prefer fixed, tool-owned names over free-form user input for global services."],"tags":["validation","filesystem","path-traversal","input-sanitization"],"backgroundTag":"invalid-identifier","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}