{"record":{"id":"3cdb28728f81a391","repo":"garrytan/gstack","slug":"state-name-must-be-alphanumeric-a-z-0-9","errorCode":null,"errorMessage":"State name must be alphanumeric (a-z, 0-9, _, -)","messagePattern":"State name must be alphanumeric \\(a-z, 0-9, _, -\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/meta-commands.ts","lineNumber":932,"sourceCode":"      // Handle --clear flag\n      if (args.includes('--clear')) {\n        for (const file of files) {\n          try { fs.unlinkSync(path.join(inboxDir, file)); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; }\n        }\n        lines.push(`Cleared ${files.length} message${files.length === 1 ? '' : 's'}.`);\n      }\n\n      return lines.join('\\n');\n    }\n\n    // ─── State ────────────────────────────────────────\n    case 'state': {\n      const [action, name] = args;\n      if (!action || !name) throw new Error('Usage: state save|load <name>');\n\n      // Sanitize name: alphanumeric + hyphens + underscores only\n      if (!/^[a-zA-Z0-9_-]+$/.test(name)) {\n        throw new Error('State name must be alphanumeric (a-z, 0-9, _, -)');\n      }\n\n      const config = resolveConfig();\n      const stateDir = path.join(config.stateDir, 'browse-states');\n      mkdirSecure(stateDir);\n      const statePath = path.join(stateDir, `${name}.json`);\n\n      if (action === 'save') {\n        const state = await bm.saveState();\n        // V1: cookies + URLs only (not localStorage — breaks on load-before-navigate)\n        const saveData = {\n          version: 1,\n          savedAt: new Date().toISOString(),\n          cookies: state.cookies,\n          pages: state.pages.map(p => ({ url: p.url, isActive: p.isActive })),\n        };\n        writeSecureFile(statePath, JSON.stringify(saveData, null, 2));\n        return `State saved: ${statePath} (${state.cookies.length} cookies, ${state.pages.length} pages)\\n⚠️  Cookies stored in plaintext. Delete when no longer needed.`;","sourceCodeStart":914,"sourceCodeEnd":950,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/meta-commands.ts#L914-L950","documentation":"The `state` command sanitizes `name` against `^[a-zA-Z0-9_-]+$` (lines 930-932) before joining it into a filesystem path (`path.join(stateDir, name + '.json')`). This prevents path traversal (`../`) and weird filenames. Any other character — slash, dot, space, unicode — triggers the error.","triggerScenarios":"`browse state save my/session`, `browse state save ../etc`, `browse state save session.2`, or any name containing spaces, dots, slashes, or non-ASCII.","commonSituations":"Trying to namespace states with slashes/dots, or passing a filename instead of a logical name.","solutions":["Use only letters, digits, underscore, and hyphen: `login-session`, `admin_v1`.","Replace separators: use `-` or `_` instead of `/` or `.`.","If you need hierarchy, encode it in the name (e.g. `tenantA-user`) rather than path segments."],"exampleFix":"// before\nbrowse state save tenantA/user.v2\n// after\nbrowse state save tenantA-user-v2","handlingStrategy":"validation","validationCode":"if (!/^[a-zA-Z0-9_-]+$/.test(name)) {\n  throw new Error(`Invalid state name '${name}': use only a-z, 0-9, _, -`);\n}","typeGuard":"const isStateName = (s: string): boolean => /^[a-zA-Z0-9_-]+$/.test(s);","tryCatchPattern":null,"preventionTips":["Generate state names from a known-safe charset (slugify free-form input).","Never accept user-supplied paths into the name slot — only logical names."],"tags":["state","validation","path-traversal","security"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}