{"record":{"id":"4c6cb1a9cfacf22f","repo":"garrytan/gstack","slug":"invalid-allowlist","errorCode":"invalid_allowlist","errorMessage":"invalid_allowlist","messagePattern":"invalid_allowlist","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ios-qa/daemon/src/allowlist.ts","lineNumber":38,"sourceCode":"  let raw: string;\n  try {\n    raw = await readFile(path, 'utf-8');\n  } catch (err: unknown) {\n    const e = err as { code?: string };\n    if (e.code === 'ENOENT') {\n      return { version: 1, entries: [] };\n    }\n    throw err;\n  }\n  // Empty-file path (mktemp default, partial write, manual `: > file`): treat\n  // as \"no entries yet\" rather than a parse error. The first grant will fill\n  // it in atomically via saveAllowlist.\n  if (raw.trim() === '') {\n    return { version: 1, entries: [] };\n  }\n  const parsed = JSON.parse(raw) as Allowlist;\n  if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {\n    throw new Error('invalid_allowlist');\n  }\n  return parsed;\n}\n\nexport async function saveAllowlist(allowlist: Allowlist, path: string = defaultAllowlistPath()): Promise<void> {\n  await mkdir(dirname(path), { recursive: true, mode: 0o700 });\n  await writeFile(path, JSON.stringify(allowlist, null, 2) + '\\n', { mode: 0o600 });\n}\n\n/**\n * Look up an identity in the allowlist. Returns the entry if present AND\n * not expired. Lookup is exact-match on canonicalized identity.\n */\nexport function findEntry(allowlist: Allowlist, identity: string): AllowlistEntry | null {\n  const now = Date.now();\n  for (const entry of allowlist.entries) {\n    if (entry.identity !== identity) continue;\n    if (entry.expires_at) {","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/ios-qa/daemon/src/allowlist.ts#L20-L56","documentation":"Thrown by loadAllowlist() when the iOS-QA allowlist JSON file at ~/.gstack/ios-qa-allowlist.json (or GSTACK_IOS_ALLOWLIST_PATH) parses successfully but is structurally invalid: parsed.version is not exactly 1, or parsed.entries is not an array. It is a deliberate fail-closed guard: a half-written or hand-edited file must not silently pass as an empty list. ENOENT and empty files are tolerated earlier; this fires only after a successful JSON.parse.","triggerScenarios":"JSON.parse succeeds but the top-level object lacks version:1 or has a non-array entries field. Concretely: someone hand-edited the file and dropped a key; a partial write left valid JSON but wrong shape; a future schema migration wrote version:2 without a loader; entries is an object/number instead of an array.","commonSituations":"Manual edit of the allowlist that broke the schema; a botched migration; an editor auto-saved a truncated object; wrong file placed at the allowlist path.","solutions":["Back up then delete ~/.gstack/ios-qa-allowlist.json and re-grant identities via gstack-ios-qa-mint grant.","If you must edit by hand, ensure the shape is exactly {\"version\":1,\"entries\":[...]} with entries as an array.","Do not bump version without updating loadAllowlist to handle the new schema.","If the file was corrupted by a crash mid-write, restore from backup or regenerate."],"exampleFix":"// before — corrupted file\n{ \"version\": 2, \"entries\": {} }\n\n// after — valid schema\n{ \"version\": 1, \"entries\": [] }","handlingStrategy":"validation","validationCode":"import { readFile } from 'fs/promises';\nasync function preflightAllowlist(path: string): Promise<void> {\n  const raw = await readFile(path, 'utf-8');\n  const parsed = JSON.parse(raw); // throws on parse error\n  if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {\n    throw new Error(`Allowlist at ${path} has invalid shape; expected {version:1, entries:[]}`);\n  }\n}","typeGuard":"function isAllowlist(v: unknown): v is { version: number; entries: unknown[] } {\n  return typeof v === 'object' && v !== null &&\n    (v as any).version === 1 && Array.isArray((v as any).entries);\n}","tryCatchPattern":"try {\n  return await loadAllowlist(path);\n} catch (e) {\n  if (e instanceof Error && e.message === 'invalid_allowlist') {\n    // Back up the corrupt file, start fresh\n    await rename(path, path + '.bak');\n    return { version: 1, entries: [] };\n  }\n  throw e;\n}","preventionTips":["Never hand-edit the allowlist JSON; use gstack-ios-qa-mint grant/revoke.","If you must edit, validate shape with a JSON schema linter first.","Back up the allowlist before any migration.","Add a schema version check at app startup to fail fast on corruption."],"tags":["ios-qa","allowlist","security","json","config"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}