{"record":{"id":"c0b1a795d9277fea","repo":"JuliusBrussee/caveman","slug":"private-device-login-requires-a-keyless-project-grant-with-a","errorCode":null,"errorMessage":"private device login requires a keyless project grant with a refresh token and delivery acknowledgement","messagePattern":"private device login requires a keyless project grant with a refresh token and delivery acknowledgement","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":9737,"sourceCode":"      }\n      await sleep(Math.max(intervalMs, retryAfterMs, 200));\n      continue;\n    }\n    if (tokenStatus >= 300 && tokenStatus < 400) throw new Error(\"device login refused a redirected token endpoint\");\n    if (tokenStatus === 429) {\n      // rateLimitAuth returns a nested cave error envelope rather than the RFC\n      // `error` string. Status is the authoritative retry signal here.\n      await sleep(Math.max(intervalMs, retryAfterMs, 200));\n      continue;\n    }\n    const accessToken = typeof tok.access_token === \"string\" ? tok.access_token : \"\";\n    if (accessToken) {\n\t  if (instance && (tokenStatus < 200 || tokenStatus >= 300 || tok.credential_kind !== \"none\" ||\n\t      [\"gateway_api_key\", \"gateway_key_id\", \"gateway_url\"].some((key) => tok[key] != null) ||\n\t      typeof tok.refresh_token !== \"string\" || !tok.refresh_token || typeof tok.project_id !== \"string\" || !tok.project_id ||\n\t      typeof tok.delivery_ack_token !== \"string\" || !tok.delivery_ack_token || typeof tok.scope !== \"string\" || !tok.scope ||\n\t      tok.scope.split(/\\s+/).some((scope) => scope === \"proxy:write\" || scope === \"sdk:write\"))) {\n\t    throw new Error(\"private device login requires a keyless project grant with a refresh token and delivery acknowledgement\");\n\t  }\n\t  const credentials: StoredCredentials = {\n\t    access_token: accessToken,\n\t    ...(typeof tok.refresh_token === \"string\" && tok.refresh_token ? { refresh_token: tok.refresh_token } : {}),\n\t    ...(typeof tok.gateway_api_key === \"string\" && tok.gateway_api_key ? { gateway_api_key: tok.gateway_api_key } : {}),\n\t    ...(typeof tok.gateway_key_id === \"string\" && tok.gateway_key_id ? { gateway_key_id: tok.gateway_key_id } : {}),\n\t    ...(typeof tok.project_id === \"string\" && tok.project_id ? { project_id: tok.project_id } : {}),\n\t  };\n\t  const tokenStore = storeCredentials(credentials);\n\t  const organizationId = orgFromToken(accessToken);\n\t  const gateway = instance ? \"\" : resolveLoginGatewayUrl(baseURL, tok, code, argv);\n\t  const saved: Config = { baseURL, token: \"\", tokenStore };\n\t  if (organizationId) saved.organizationId = organizationId;\n\t  if (credentials.project_id) saved.projectId = credentials.project_id;\n\t  if (gateway) saved.gatewayUrl = gateway;\n\t  // Persist the complete local login state before the server-side receipt fence:\n\t  // an ACK may permanently purge the replay bundle, so a config write that fails\n\t  // must leave the grant retryable rather than acknowledging an undiscoverable","sourceCodeStart":9719,"sourceCodeEnd":9755,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L9719-L9755","documentation":"For private instances the CLI enforces that the token response is a 'keyless project grant': no gateway API key material, a non-empty refresh_token, project_id, delivery_ack_token, and a scope without write privileges (proxy:write / sdk:write). If the status is not 2xx or any invariant fails, this error is thrown. It guards private instances from receiving over-privileged or undeliverable credentials.","triggerScenarios":"A private-instance device token poll returns a payload where credential_kind is not \"none\", any gateway_api_key/gateway_key_id/gateway_url is present, refresh_token/project_id/delivery_ack_token/scope is missing or empty, or the scope contains proxy:write or sdk:write; or the HTTP status is outside 200–299.","commonSituations":"Authorization server misconfigured to issue gateway API keys for private instances; missing refresh-token grant on the client; overly broad scope configuration on the server; wrong audience/authorization server returning a cloud-style token bundle.","solutions":["Configure the private instance's authorization server to issue keyless project grants (credential_kind \"none\") with refresh tokens","Narrow the issued scopes so they exclude proxy:write and sdk:write","Ensure the token response includes non-empty refresh_token, project_id, delivery_ack_token, and scope","Verify the --instance points at the correct, properly configured authorization server"],"exampleFix":"// before\n{ \"credential_kind\": \"gateway_api_key\", \"gateway_api_key\": \"...\", \"scope\": \"proxy:write\" }\n// after\n{ \"credential_kind\": \"none\", \"refresh_token\": \"...\", \"project_id\": \"...\", \"delivery_ack_token\": \"...\", \"scope\": \"read\" }","handlingStrategy":"validation","validationCode":"function isValidKeylessGrant(tok) {\n  return tok.credential_kind === \"none\" && typeof tok.refresh_token === \"string\" && tok.refresh_token &&\n    typeof tok.project_id === \"string\" && tok.project_id && typeof tok.delivery_ack_token === \"string\" && tok.delivery_ack_token &&\n    typeof tok.scope === \"string\" && tok.scope && !tok.scope.split(/\\s+/).some(s => s === \"proxy:write\" || s === \"sdk:write\");\n}","typeGuard":"function isKeylessGrant(tok) { return !!tok && tok.credential_kind === \"none\" && !tok.gateway_api_key && !tok.gateway_key_id && !tok.gateway_url; }","tryCatchPattern":"try { await login({ instance }) } catch (e) { if (e.message.includes(\"keyless project grant\")) console.error(\"Instance issued an over-privileged or incomplete token; fix the authorization server config\"); }","preventionTips":["Configure the private IdP to issue credential_kind \"none\" with refresh tokens","Keep issued scopes free of proxy:write and sdk:write for this client","Validate a token response against the grant shape after any IdP upgrade"],"tags":["oauth","device-flow","validation","security"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}