diff --git a/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap b/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap
index 1eb91eb9fcbdb6567b10e1eb533065d2969bd1aa..6b9b5b78f2eb9df31df3ae7f99e3643e25156b34 100644
--- a/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap
+++ b/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap
@@ -2,6 +2,7 @@
 
 exports[`modules/datasource/npm/index should fetch package info from custom registry 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://npm.mycustomregistry.com",
   "releases": [
@@ -25,6 +26,7 @@ exports[`modules/datasource/npm/index should fetch package info from custom regi
 
 exports[`modules/datasource/npm/index should fetch package info from npm 1`] = `
 {
+  "isPrivate": false,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -48,6 +50,7 @@ exports[`modules/datasource/npm/index should fetch package info from npm 1`] = `
 
 exports[`modules/datasource/npm/index should handle foobar 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -71,6 +74,7 @@ exports[`modules/datasource/npm/index should handle foobar 1`] = `
 
 exports[`modules/datasource/npm/index should handle no time 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -93,6 +97,7 @@ exports[`modules/datasource/npm/index should handle no time 1`] = `
 
 exports[`modules/datasource/npm/index should not send an authorization header if public package 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -116,6 +121,7 @@ exports[`modules/datasource/npm/index should not send an authorization header if
 
 exports[`modules/datasource/npm/index should parse repo url (string) 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -134,6 +140,7 @@ exports[`modules/datasource/npm/index should parse repo url (string) 1`] = `
 
 exports[`modules/datasource/npm/index should parse repo url 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -152,6 +159,7 @@ exports[`modules/datasource/npm/index should parse repo url 1`] = `
 
 exports[`modules/datasource/npm/index should replace any environment variable in npmrc 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.from-env.com",
   "releases": [
@@ -181,6 +189,7 @@ exports[`modules/datasource/npm/index should return deprecated 1`] = `
 
 Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.",
   "deprecationSource": "npm",
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -212,6 +221,7 @@ Marking the latest version of an npm package as deprecated results in the entire
 
 exports[`modules/datasource/npm/index should send an authorization header if provided 1`] = `
 {
+  "isPrivate": true,
   "name": "@foobar/core",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -235,6 +245,7 @@ exports[`modules/datasource/npm/index should send an authorization header if pro
 
 exports[`modules/datasource/npm/index should use default registry if missing from npmrc 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
@@ -258,6 +269,7 @@ exports[`modules/datasource/npm/index should use default registry if missing fro
 
 exports[`modules/datasource/npm/index should use host rules by baseUrl if provided 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://npm.mycustomregistry.com/_packaging/mycustomregistry/npm/registry",
   "releases": [
@@ -281,6 +293,7 @@ exports[`modules/datasource/npm/index should use host rules by baseUrl if provid
 
 exports[`modules/datasource/npm/index should use host rules by hostName if provided 1`] = `
 {
+  "isPrivate": true,
   "name": "foobar",
   "registryUrl": "https://npm.mycustomregistry.com",
   "releases": [
diff --git a/lib/modules/datasource/npm/get.ts b/lib/modules/datasource/npm/get.ts
index 54e44742a1d987cc04a88edd5623574b64ee3e22..4a66f58f519983d9497d6a5864da8f58d53c9006 100644
--- a/lib/modules/datasource/npm/get.ts
+++ b/lib/modules/datasource/npm/get.ts
@@ -171,19 +171,12 @@ export async function getDependency(
       return release;
     });
     logger.trace({ dep }, 'dep');
-    // serialize first before saving
-    // TODO: use dynamic detection of public repos instead of a static list (#9587)
-    const whitelistedPublicScopes = [
-      '@graphql-codegen',
-      '@storybook',
-      '@types',
-      '@typescript-eslint',
-    ];
+    const cacheControl = raw.headers?.['cache-control'];
     if (
-      !raw.authorization &&
-      (whitelistedPublicScopes.includes(packageName.split('/')[0]) ||
-        !packageName.startsWith('@'))
+      is.nonEmptyString(cacheControl) &&
+      regEx(/(^|,)\s*public\s*(,|$)/).test(cacheControl)
     ) {
+      dep.isPrivate = false;
       const cacheData = { softExpireAt, etag };
       await packageCache.set(
         cacheNamespace,
@@ -191,6 +184,8 @@ export async function getDependency(
         { ...dep, cacheData },
         etag ? cacheHardTtlMinutes : cacheMinutes
       );
+    } else {
+      dep.isPrivate = true;
     }
     return dep;
   } catch (err) {
diff --git a/lib/modules/datasource/npm/index.spec.ts b/lib/modules/datasource/npm/index.spec.ts
index c40aff8a397f0526a53b2232c2885506e3267277..6dd04699db97b6bdc1e64832014a384fecc7af59 100644
--- a/lib/modules/datasource/npm/index.spec.ts
+++ b/lib/modules/datasource/npm/index.spec.ts
@@ -64,9 +64,10 @@ describe('modules/datasource/npm/index', () => {
     httpMock
       .scope('https://registry.npmjs.org')
       .get('/foobar')
-      .reply(200, npmResponse);
+      .reply(200, npmResponse, { 'Cache-control': 'public, expires=300' });
     const res = await getPkgReleases({ datasource, depName: 'foobar' });
     expect(res).toMatchSnapshot();
+    expect(res?.isPrivate).toBeFalse();
   });
 
   it('should parse repo url', async () => {
@@ -155,6 +156,7 @@ describe('modules/datasource/npm/index', () => {
       .reply(200, npmResponse);
     const res = await getPkgReleases({ datasource, depName: 'foobar' });
     expect(res).toMatchSnapshot();
+    expect(res?.isPrivate).toBeTrue();
   });
 
   it('should handle no time', async () => {
@@ -307,6 +309,7 @@ describe('modules/datasource/npm/index', () => {
     const npmrc = `registry=https://npm.mycustomregistry.com/`;
     const res = await getPkgReleases({ datasource, depName: 'foobar', npmrc });
     expect(res).toMatchSnapshot();
+    expect(res?.isPrivate).toBeTrue();
   });
 
   it('should replace any environment variable in npmrc', async () => {