{"record":{"id":"62ee3dae4fa106ca","repo":"santifer/career-ops","slug":"wttj-unexpected-algolia-api-key-shape","errorCode":null,"errorMessage":"wttj: unexpected Algolia api key shape","messagePattern":"wttj: unexpected Algolia api key shape","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/wttj.mjs","lineNumber":97,"sourceCode":"  const start = text.indexOf('{');\n  const end = text.lastIndexOf('}');\n  if (start === -1 || end <= start) throw new Error('wttj: /api/env payload has no JSON object');\n  let env;\n  try {\n    env = JSON.parse(text.slice(start, end + 1));\n  } catch {\n    throw new Error('wttj: /api/env payload is not valid JSON');\n  }\n  const appId = typeof env.PUBLIC_ALGOLIA_APPLICATION_ID === 'string' ? env.PUBLIC_ALGOLIA_APPLICATION_ID.trim() : '';\n  const apiKey = typeof env.PUBLIC_ALGOLIA_API_KEY_CLIENT === 'string' ? env.PUBLIC_ALGOLIA_API_KEY_CLIENT.trim() : '';\n  // App ids are short alphanumerics; validating keeps the derived Algolia\n  // hostname from being attacker-shaped if the env payload ever changes.\n  if (!/^[A-Z0-9]{6,16}$/i.test(appId)) throw new Error(`wttj: unexpected Algolia app id \"${appId}\"`);\n  // The key is only ever sent as a request header (never used to build a\n  // host), so don't over-constrain its format — WTTJ may rotate to a longer\n  // or non-hex (e.g. secured/base64) client key. Length bounds only.\n  if (!apiKey || apiKey.length < 16 || apiKey.length > 500) {\n    throw new Error('wttj: unexpected Algolia api key shape');\n  }\n  return { appId, apiKey };\n}\n\n/**\n * Normalize a single Algolia hit. Exported for tests.\n *\n * Field mapping → normalized Job shape:\n *   - title:    `name`\n *   - url:      /en/companies/{organization.slug}/jobs/{slug} on the WTTJ site\n *   - company:  `organization.name`\n *   - location: offices[0] city+country, with \", Remote\" appended when the\n *               posting allows fulltime remote\n *   - postedAt: `published_at_timestamp` (epoch seconds → ms)\n *   - salary:   {min, max, currency} from salary_yearly_minimum/salary_maximum\n *\n * @param {any} h\n * @returns {{ title: string, url: string, company: string, location: string, postedAt?: number, salary?: {min: number, max: number, currency: string} } | null}","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/wttj.mjs#L79-L115","documentation":"parseEnvPayload validates the Algolia client API key from WTTJ's /api/env payload with only length bounds (non-empty, 16-500 chars). The key is only sent as a request header, never used to build a hostname, so the format is deliberately not over-constrained — but an absent, empty, or absurdly short/long key still fails this guard. It indicates the env payload did not contain a usable PUBLIC_ALGOLIA_API_KEY_CLIENT value.","triggerScenarios":"The /api/env response lacks PUBLIC_ALGOLIA_API_KEY_CLIENT (field not a string, or empty/whitespace after trim), or the string is shorter than 16 chars or longer than 500 chars.","commonSituations":"WTTJ rotates or renames the client key field; an anti-bot challenge page or cached empty body is returned instead of the env JSON; a proxy strips the field; the key was truncated by an intermediary.","solutions":["Re-fetch the /api/env payload manually and confirm PUBLIC_ALGOLIA_API_KEY_CLIENT is present and its actual length","If the field was renamed/removed, update the property name in parseEnvPayload","Check for proxies/CDN caches returning stale or sanitized responses; bypass or purge them","If WTTJ rotated to a shorter key format, adjust the >=16 lower bound after verifying the real key shape"],"exampleFix":"// before\nif (!apiKey || apiKey.length < 16 || apiKey.length > 500) {\n  throw new Error('wttj: unexpected Algolia api key shape');\n}\n// after (accept rotated shorter key, verified from live payload)\nif (!apiKey || apiKey.length < 12 || apiKey.length > 500) {\n  throw new Error('wttj: unexpected Algolia api key shape');\n}","handlingStrategy":"validation","validationCode":"const env = JSON.parse(envText);\nconst key = env?.PUBLIC_ALGOLIA_API_KEY_CLIENT;\nif (typeof key !== 'string' || key.trim().length < 16 || key.trim().length > 500) {\n  throw new Error('env payload lacks a usable Algolia client key — check /api/env output');\n}","typeGuard":"function hasAlgoliaClientKey(env) {\n  const k = env?.PUBLIC_ALGOLIA_API_KEY_CLIENT;\n  return typeof k === 'string' && k.trim().length >= 16 && k.trim().length <= 500;\n}","tryCatchPattern":"try {\n  await scanWttj(entry);\n} catch (e) {\n  if (e.message === 'wttj: unexpected Algolia api key shape') {\n    console.warn('PUBLIC_ALGOLIA_API_KEY_CLIENT missing or out of bounds — WTTJ may have rotated it.');\n  } else throw e;\n}","preventionTips":["Treat the key as opaque: only length checks, never format assumptions (it may become base64/secured)","Diff a recorded /api/env fixture against live output when the key check fails","Ensure HTTP caches/proxies don't strip response fields; test from a clean network","Keep the error message distinct from the app-id error so triage is immediate"],"tags":["validation","scraper","algolia","wttj","api-key"],"backgroundTag":"missing-env-var","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}