{"record":{"id":"f4867a2773c7e7c9","repo":"paperclipai/paperclip","slug":"set-both-namespaced-page-uploader-credential-variables","errorCode":null,"errorMessage":"Set both namespaced page uploader credential variables","messagePattern":"Set both namespaced page uploader credential variables","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/publish-announcements.ts","lineNumber":89,"sourceCode":"    \"--content-type\", file.contentType, \"--cache-control\", file.cacheControl];\n}\n\nasync function main() {\n  const { sourceDirectory, staging, publish } = parseAnnouncementPublishArgs(process.argv.slice(2));\n  const hostPrefix = process.env.PAPERCLIP_PAGE_DEFAULT_PREFIX;\n  const prepared = await prepareAnnouncementPublish(sourceDirectory, staging, hostPrefix);\n  const bucket = process.env.PAPERCLIP_PAGE_BUCKET;\n  const baseUrl = process.env.PAPERCLIP_PAGE_BASE_URL?.replace(/\\/+$/, \"\") ?? \"https://pages.paperclip.ing\";\n  const url = `${baseUrl}/${announcementPublishPrefix(staging, hostPrefix)}/current.json`;\n  const parsed = new URL(url);\n  if (parsed.protocol !== \"https:\" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error(\"Invalid public base URL\");\n  console.log(JSON.stringify({ mode: publish ? \"publish\" : \"dry-run\", target: staging ? `staging/${staging}` : \"production\", bucket: bucket ?? \"(unset)\", url, announcementId: prepared.manifest.announcement?.id ?? null, files: prepared.files }, null, 2));\n  if (!publish) return;\n  if (!bucket) throw new Error(\"Set PAPERCLIP_PAGE_BUCKET before publishing\");\n  const env = { ...process.env };\n  const key = env.PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID;\n  const secret = env.PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY;\n  if (Boolean(key) !== Boolean(secret)) throw new Error(\"Set both namespaced page uploader credential variables\");\n  if (key && secret) {\n    env.AWS_ACCESS_KEY_ID = key;\n    env.AWS_SECRET_ACCESS_KEY = secret;\n    delete env.AWS_SESSION_TOKEN;\n    if (env.PAPERCLIP_PAGE_AWS_SESSION_TOKEN) env.AWS_SESSION_TOKEN = env.PAPERCLIP_PAGE_AWS_SESSION_TOKEN;\n  } else if (env.PAPERCLIP_PAGE_AWS_PROFILE) {\n    delete env.AWS_ACCESS_KEY_ID;\n    delete env.AWS_SECRET_ACCESS_KEY;\n    delete env.AWS_SESSION_TOKEN;\n    env.AWS_PROFILE = env.PAPERCLIP_PAGE_AWS_PROFILE;\n  }\n  // Only validated files, assets before manifest; credentials are scoped to AWS.\n  for (const file of prepared.files) execFileSync(\"aws\", announcementUploadArgs(bucket, file), { env, stdio: \"pipe\" });\n  console.log(\"Uploaded. Checking the public manifest (CDN propagation can take five minutes)…\");\n  for (let attempt = 0; attempt < 23; attempt++) {\n    try {\n      const response = await fetch(url, { signal: AbortSignal.timeout(10_000), credentials: \"omit\", redirect: \"error\" });\n      const body = announcementManifestSchema.parse(await response.json());","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/publish-announcements.ts#L71-L107","documentation":"AWS credentials may be provided via namespaced PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID / PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY (mapped onto the standard AWS_* names for the upload). This error means exactly one of the pair is set, i.e. they must be provided together or not at all.","triggerScenarios":"Setting PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID but forgetting PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY (or vice versa) when not relying on ambient AWS credentials/profile.","commonSituations":"Partial secret injection in CI where only one of the two variables was configured; hand-copying credentials and missing the second line; rotating keys and updating only one variable.","solutions":["Set both PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID and PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY together","Or unset both to fall back to ambient AWS credentials/profile (e.g. PAPERCLIP_PAGE_AWS_PROFILE or default chain)","Fix the CI secrets mapping so both variables are injected from their paired secrets","Verify both with: printenv | grep PAPERCLIP_PAGE_AWS"],"exampleFix":"// before\nexport PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID=AKIA...\n// after\nexport PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID=AKIA...\nexport PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY=****","handlingStrategy":"validation","validationCode":"const k = !!process.env.PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID, s = !!process.env.PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY;\nif (k !== s) throw new Error(\"set both or neither of the namespaced page uploader credentials\");","typeGuard":null,"tryCatchPattern":"try { await main(); } catch (e) { if (e.message.includes(\"namespaced page uploader credential\")) { /* set/unset the credential pair together */ } }","preventionTips":["Treat the access key and secret as one unit: set or unset both together","Map CI secrets in pairs and verify both are injected before the publish step","When rotating credentials, update both variables in the same change","Prefer a named profile (PAPERCLIP_PAGE_AWS_PROFILE) over hand-set keys to avoid partial configuration"],"tags":["env","credentials","aws"],"backgroundTag":"missing-credentials","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}