{"record":{"id":"2e82f8a4d0d6e5b9","repo":"paperclipai/paperclip","slug":"grant-credential-invalid","errorCode":"grant_credential_invalid","errorMessage":"Personal authorization has an invalid credential","messagePattern":"Personal authorization has an invalid credential","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":422,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":3524,"sourceCode":"        scope: companySecrets.scope,\n        ownerUserId: companySecrets.ownerUserId,\n        userSecretDefinitionId: companySecrets.userSecretDefinitionId,\n      })\n      .from(companySecrets)\n      .where(\n        and(\n          eq(companySecrets.id, ref.secretId),\n          eq(companySecrets.companyId, connection.companyId),\n        ),\n      )\n      .limit(1);\n    if (\n      !secret ||\n      secret.scope !== \"user\" ||\n      secret.ownerUserId !== grant.subjectUserId ||\n      !secret.userSecretDefinitionId\n    ) {\n      throw new ToolGatewayHttpError(\n        422,\n        \"Personal authorization has an invalid credential\",\n        \"grant_credential_invalid\",\n        {\n          connectionId: connection.id,\n          grantId: grant.id,\n          credential: configPath,\n        },\n      );\n    }\n    const resolved = await secrets.resolveUserSecretValue(\n      connection.companyId,\n      {\n        definitionId: secret.userSecretDefinitionId,\n        responsibleUserId: grant.subjectUserId,\n        version: ref.versionSelector ?? \"latest\",\n        required: ref.required ?? true,\n      },","sourceCodeStart":3506,"sourceCodeEnd":3542,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/tool-gateway.ts#L3506-L3542","documentation":"A connection grant of kind 'user' references a credential secret, but the secret row looked up from companySecrets is either missing, not scoped to 'user', not owned by the grant's subjectUserId, or lacks a userSecretDefinitionId. The tool gateway throws this 422 before resolving the personal credential, because a personal authorization grant may only use a user-scoped secret owned by the subject user and tied to a user secret definition.","triggerScenarios":"resolveGrantSecretValue is called for a user-kind grant whose grant.credentialSecretRefs point to a secret that (a) does not exist in companySecrets for the company, (b) has scope 'company' instead of 'user', (c) has ownerUserId different from grant.subjectUserId, or (d) was created without a userSecretDefinitionId linkage.","commonSituations":"An operator manually copied a company-level secret into a personal grant's credential refs; a user's OAuth callback created the secret but the definition linkage migration was skipped; secrets were re-created after a company clone so the grant references a stale secret id; a grant was reassigned to a different user without updating the secret owner.","solutions":["Re-create the personal authorization so the OAuth callback regenerates the user-scoped secret with the correct ownerUserId and userSecretDefinitionId.","Inspect the companySecrets row for ref.secretId and fix scope ('user'), ownerUserId (must equal grant.subjectUserId), and userSecretDefinitionId.","If the grant should not be personal, change it to a non-user kind (e.g. agent/company) so the generic resolveSecretValue path is used instead.","Verify the user secret definition exists for the connection's credential; re-register the definition and relink the secret."],"exampleFix":"// before: company-scoped secret attached to a personal grant\n{ id: 'sec_123', scope: 'company', ownerUserId: null, userSecretDefinitionId: null }\n// after: user-scoped secret owned by the grant subject\n{ id: 'sec_123', scope: 'user', ownerUserId: 'usr_42', userSecretDefinitionId: 'usd_api_key' }","handlingStrategy":"validation","validationCode":"const [secret] = await db.select().from(companySecrets).where(and(eq(companySecrets.id, ref.secretId), eq(companySecrets.companyId, companyId)));\nif (!secret || secret.scope !== 'user' || secret.ownerUserId !== grant.subjectUserId || !secret.userSecretDefinitionId) {\n  throw new Error(`Grant ${grant.id} references an invalid credential for ${ref.secretId}`);\n}","typeGuard":"function isValidUserSecret(s: typeof companySecrets.$inferSelect | undefined, subjectUserId: string): s is typeof companySecrets.$inferSelect & { scope: 'user'; ownerUserId: string; userSecretDefinitionId: string } {\n  return !!s && s.scope === 'user' && s.ownerUserId === subjectUserId && typeof s.userSecretDefinitionId === 'string';\n}","tryCatchPattern":"try {\n  const value = await resolveCredentialHeaders(session, connection, grant);\n} catch (e) {\n  if (e instanceof ToolGatewayHttpError && e.code === 'grant_credential_invalid') {\n    // surface 're-authorize personal connection' to the user\n  }\n  throw e;\n}","preventionTips":["Always create personal-grant secrets through the OAuth callback flow, never by manually editing companySecrets.","Assert scope === 'user' and ownerUserId === subjectUserId at grant creation time.","Re-run secret-linkage checks after company clones or restores."],"tags":["authorization","credentials","data-integrity","http-422"],"backgroundTag":"invalid-state-transition","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"}