{"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":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/wttj.mjs","lineNumber":72,"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":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/wttj.mjs#L54-L90","documentation":"The parsed PUBLIC_ALGOLIA_API_KEY_CLIENT must be non-empty and 16–500 chars. The bounds are deliberately wide (the comment notes WTTJ may rotate to longer/base64/secured keys) since the key is only ever sent as a request header, never used to build a host. Throws when the key is missing/renamed or outside the length bounds.","triggerScenarios":"/api/env no longer contains PUBLIC_ALGOLIA_API_KEY_CLIENT; the field was renamed; WTTJ rotated to a key shorter than 16 or longer than 500 chars (extremely unlikely); a truncated /api/env response cut off the key.","commonSituations":"WTTJ renamed the env key; the env object parsed but the key field is absent; a truncated response.","solutions":["Fetch /api/env and verify PUBLIC_ALGOLIA_API_KEY_CLIENT is present and non-empty.","If the key was renamed, update the field read in parseEnvPayload.","If the key genuinely grew past 500 chars, raise the upper bound after confirming.","If /api/env was truncated, retry the fetch."],"exampleFix":"// if the env key is renamed:\n// before\nconst apiKey = typeof env.PUBLIC_ALGOLIA_API_KEY_CLIENT === \"string\" ? env.PUBLIC_ALGOLIA_API_KEY_CLIENT.trim() : \"\";\n// after\nconst apiKey = typeof env.PUBLIC_ALGOLIA_SEARCH_KEY === \"string\" ? env.PUBLIC_ALGOLIA_SEARCH_KEY.trim() : \"\";","handlingStrategy":"validation","validationCode":"if (typeof parsedApiKey !== \"string\" || parsedApiKey.length < 16 || parsedApiKey.length > 500)\n  console.warn(\"wttj api key shape changed — length:\", parsedApiKey?.length);","typeGuard":"const isValidAlgoliaApiKey = (s) => typeof s === \"string\" && s.length >= 16 && s.length <= 500;","tryCatchPattern":"try { parseEnvPayload(envText); }\ncatch (err) {\n  if (/Algolia api key shape/.test(err.message)) { logUpstreamChange(\"wttj\", err.message); }\n  throw err;\n}","preventionTips":["Alert on this error to catch a key rename or rotation.","Re-fetch /api/env each run so a rotated key self-heals.","Only widen the length bounds after confirming the new key shape."],"tags":["api-contract","wttj","algolia","validation","key-rotation"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}