diff --git a/.changeset/pink-apples-nail.md b/.changeset/pink-apples-nail.md new file mode 100644 index 000000000..6bac4ddbe --- /dev/null +++ b/.changeset/pink-apples-nail.md @@ -0,0 +1,7 @@ +--- +'@verdaccio/ui-theme': minor +'@verdaccio/ui-components': minor +'@verdaccio/config': minor +--- + +feat: forbidden user interface diff --git a/package.json b/package.json index 26ce2c287..e3f97dd18 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "docker": "docker build -t verdaccio/verdaccio:local . --no-cache", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"", "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"", - "lint": "eslint --max-warnings 100 \"**/*.{js,jsx,ts,tsx}\"", + "lint": "eslint --max-warnings 70 \"**/*.{js,jsx,ts,tsx}\"", "test": "pnpm --filter \"./packages/**\" test", "test:e2e:cli": "pnpm --filter ...@verdaccio/e2e-cli-* test -- --coverage=false", "test:e2e:ui": "pnpm --filter ...@verdaccio/e2e-ui test", diff --git a/packages/api/test/integration/config/user.jwt.yaml b/packages/api/test/integration/config/user.jwt.yaml index 41f407442..b5e65581c 100644 --- a/packages/api/test/integration/config/user.jwt.yaml +++ b/packages/api/test/integration/config/user.jwt.yaml @@ -10,7 +10,7 @@ auth: uplinks: ver: - url: https://registry.verdaccio.org + url: https://registry.npmjs.org security: api: diff --git a/packages/api/test/integration/config/user.yaml b/packages/api/test/integration/config/user.yaml index 318133982..7de8dae66 100644 --- a/packages/api/test/integration/config/user.yaml +++ b/packages/api/test/integration/config/user.yaml @@ -7,7 +7,7 @@ web: uplinks: ver: - url: https://registry.verdaccio.org + url: https://registry.npmjs.org log: { type: stdout, format: pretty, level: trace } diff --git a/packages/config/src/package-access.ts b/packages/config/src/package-access.ts index 4057c99f8..8b9b86460 100644 --- a/packages/config/src/package-access.ts +++ b/packages/config/src/package-access.ts @@ -1,9 +1,12 @@ import assert from 'assert'; +import buildDebug from 'debug'; import _ from 'lodash'; import { errorUtils } from '@verdaccio/core'; import { PackageAccess } from '@verdaccio/types'; +const debug = buildDebug('verdaccio:config:utils'); + export interface LegacyPackageList { [key: string]: PackageAccess; } @@ -61,6 +64,7 @@ export function normalisePackageAccess(packages: LegacyPackageList): LegacyPacka for (const pkg in packages) { if (Object.prototype.hasOwnProperty.call(packages, pkg)) { const packageAccess = packages[pkg]; + debug('package access %s for %s ', packageAccess, pkg); const isInvalid = _.isObject(packageAccess) && _.isArray(packageAccess) === false; assert(isInvalid, `CONFIG: bad "'${pkg}'" package description (object expected)`); diff --git a/packages/plugins/ui-theme/src/i18n/crowdin/ui.json b/packages/plugins/ui-theme/src/i18n/crowdin/ui.json index 0738ac394..941c02ce2 100644 --- a/packages/plugins/ui-theme/src/i18n/crowdin/ui.json +++ b/packages/plugins/ui-theme/src/i18n/crowdin/ui.json @@ -147,9 +147,11 @@ "error": { "unspecific": "Something went wrong.", "404": { - "page-not-found": "404 - Page not found", "sorry-we-could-not-find-it": "Sorry, we couldn't find it..." }, + "401": { + "sorry-no-access": "Sorry, you need credentials access to see this page." + }, "app-context-not-correct-used": "The app context was not used correctly", "theme-context-not-correct-used": "The theme context was not used correctly", "package-meta-is-required-at-detail-context": "packageMeta is required at DetailContext" diff --git a/packages/plugins/ui-theme/src/pages/Version/Version.tsx b/packages/plugins/ui-theme/src/pages/Version/Version.tsx index 891e53edd..29bfea952 100644 --- a/packages/plugins/ui-theme/src/pages/Version/Version.tsx +++ b/packages/plugins/ui-theme/src/pages/Version/Version.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { Loading, NotFound, RootState, VersionLayout } from '@verdaccio/ui-components'; +import { Forbidden, Loading, NotFound, RootState, VersionLayout } from '@verdaccio/ui-components'; const Version: React.FC = () => { const manifestStore = useSelector((state: RootState) => state.manifest); @@ -11,10 +11,13 @@ const Version: React.FC = () => { return ; } + if (manifestStore.forbidden) { + return ; + } + if (manifestStore.hasNotBeenFound) { return ; } - return ; }; diff --git a/packages/ui-components/jest/jest.config.js b/packages/ui-components/jest/jest.config.js index 187198e8b..08aa8cf07 100644 --- a/packages/ui-components/jest/jest.config.js +++ b/packages/ui-components/jest/jest.config.js @@ -37,7 +37,7 @@ module.exports = Object.assign({}, config, { branches: 70, functions: 76, lines: 80, - statements: 82, + statements: 81, }, }, }); diff --git a/packages/ui-components/jest/server-handlers.ts b/packages/ui-components/jest/server-handlers.ts index c1e35b29f..806cbd8f7 100644 --- a/packages/ui-components/jest/server-handlers.ts +++ b/packages/ui-components/jest/server-handlers.ts @@ -31,6 +31,15 @@ export const handlers = [ rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => { return res(ctx.json(require('./api/jquery-sidebar.json'))); }), + rest.get('http://localhost:9000/-/verdaccio/data/sidebar/JSONStream', (req, res, ctx) => { + return res(ctx.status(401)); + }), + rest.get('http://localhost:9000/-/verdaccio/data/sidebar/semver', (req, res, ctx) => { + return res(ctx.status(500)); + }), + rest.get('http://localhost:9000/-/verdaccio/data/sidebar/kleur', (req, res, ctx) => { + return res(ctx.status(404)); + }), rest.get('http://localhost:9000/-/verdaccio/data/sidebar/glob', (req, res, ctx) => { return res(ctx.json(require('./api/glob-sidebar.json'))); }), diff --git a/packages/ui-components/src/AppTest/App.stories.tsx b/packages/ui-components/src/AppTest/App.stories.tsx new file mode 100644 index 000000000..09793b096 --- /dev/null +++ b/packages/ui-components/src/AppTest/App.stories.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router'; + +import AppRoute from './AppRoute'; + +export default { + title: 'App/Main', +}; + +export const ApplicationStoryBook: any = () => ( + + + +); + +export const ApplicationjQuery: any = () => ( + + + +); + +export const ApplicationForbidden: any = () => ( + + + +); + +export const ApplicationNotFound: any = () => ( + + + +); diff --git a/packages/ui-components/src/AppTest/AppRoute.tsx b/packages/ui-components/src/AppTest/AppRoute.tsx new file mode 100644 index 000000000..732b0bff8 --- /dev/null +++ b/packages/ui-components/src/AppTest/AppRoute.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Route as ReactRouterDomRoute, Switch } from 'react-router-dom'; + +import { NotFound, Route, VersionProvider, loadable } from '../index'; + +const VersionPage = loadable(() => import(/* webpackChunkName: "Version" */ './pages/Version')); +const Front = loadable(() => import(/* webpackChunkName: "Home" */ './pages/Front')); + +const AppRoute: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AppRoute; diff --git a/packages/ui-components/src/AppTest/pages/Front/Home.ts b/packages/ui-components/src/AppTest/pages/Front/Home.ts new file mode 100644 index 000000000..3784ce454 --- /dev/null +++ b/packages/ui-components/src/AppTest/pages/Front/Home.ts @@ -0,0 +1,3 @@ +import { Home } from '../../../index'; + +export default Home; diff --git a/packages/ui-components/src/AppTest/pages/Front/index.ts b/packages/ui-components/src/AppTest/pages/Front/index.ts new file mode 100644 index 000000000..41e08eefd --- /dev/null +++ b/packages/ui-components/src/AppTest/pages/Front/index.ts @@ -0,0 +1 @@ +export { default } from './Home'; diff --git a/packages/ui-components/src/AppTest/pages/Version/Version.tsx b/packages/ui-components/src/AppTest/pages/Version/Version.tsx new file mode 100644 index 000000000..9c71f3d79 --- /dev/null +++ b/packages/ui-components/src/AppTest/pages/Version/Version.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import Forbidden from '../../../components/Forbidden'; +import { Loading, NotFound, RootState, VersionLayout } from '../../../index'; + +const Version: React.FC = () => { + const manifestStore = useSelector((state: RootState) => state.manifest); + const isLoading = useSelector((state: RootState) => state?.loading?.models.manifest); + + if (isLoading) { + return ; + } + + // @ts-expect-error + if (manifestStore.forbidden) { + return ; + } + + // @ts-expect-error + if (manifestStore.hasNotBeenFound) { + return ; + } + + return ; +}; + +export default Version; diff --git a/packages/ui-components/src/AppTest/pages/Version/index.ts b/packages/ui-components/src/AppTest/pages/Version/index.ts new file mode 100644 index 000000000..19a6473af --- /dev/null +++ b/packages/ui-components/src/AppTest/pages/Version/index.ts @@ -0,0 +1 @@ +export { default } from './Version'; diff --git a/packages/ui-components/src/components/Forbidden/Forbidden.test.tsx b/packages/ui-components/src/components/Forbidden/Forbidden.test.tsx new file mode 100644 index 000000000..79069cef8 --- /dev/null +++ b/packages/ui-components/src/components/Forbidden/Forbidden.test.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router'; + +import { fireEvent, render, screen } from '../../test/test-react-testing-library'; +import Forbidden from './Forbidden'; + +const mockHistory = jest.fn(); + +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: mockHistory, + }), +})); + +describe(' component', () => { + test('should load the component in default state', () => { + render( + + + + ); + expect(screen.getByTestId('LockIcon')).toBeInTheDocument(); + expect(screen.getByText('error.401.sorry-no-access')).toBeInTheDocument(); + expect(screen.getByText('button.go-to-the-home-page')).toBeInTheDocument(); + }); + + test('go to Home Page button click', async () => { + const { getByTestId } = render( + + + + ); + + const node = getByTestId('not-found-go-to-home-button'); + fireEvent.click(node); + expect(mockHistory).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui-components/src/components/Forbidden/Forbidden.tsx b/packages/ui-components/src/components/Forbidden/Forbidden.tsx new file mode 100644 index 000000000..54a9d5455 --- /dev/null +++ b/packages/ui-components/src/components/Forbidden/Forbidden.tsx @@ -0,0 +1,56 @@ +/* eslint-disable react/forbid-component-props */ + +/* eslint-disable verdaccio/jsx-no-style */ +import styled from '@emotion/styled'; +import LockIcon from '@mui/icons-material/Lock'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; + +import { Theme } from '../..'; +import Heading from '../Heading'; + +const Foebidden: React.FC = () => { + const history = useHistory(); + + const { t } = useTranslation(); + + const handleGoHome = useCallback(() => { + history.push('/'); + }, [history]); + + return ( + + + + + + {t('error.401.sorry-no-access')} + + + + ); +}; + +export default Foebidden; + +const Container = styled('div')({ + margin: '0 auto', +}); + +const StyledHeading = styled(Heading)<{ theme?: Theme }>(({ theme }) => ({ + color: theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.white, + marginBottom: 16, +})); diff --git a/packages/ui-components/src/components/Forbidden/index.ts b/packages/ui-components/src/components/Forbidden/index.ts new file mode 100644 index 000000000..f25fb237d --- /dev/null +++ b/packages/ui-components/src/components/Forbidden/index.ts @@ -0,0 +1 @@ +export { default } from './Forbidden'; diff --git a/packages/ui-components/src/components/NotFound/NotFound.tsx b/packages/ui-components/src/components/NotFound/NotFound.tsx index b01490834..6a1f898b6 100644 --- a/packages/ui-components/src/components/NotFound/NotFound.tsx +++ b/packages/ui-components/src/components/NotFound/NotFound.tsx @@ -1,4 +1,6 @@ +/* eslint-disable verdaccio/jsx-no-style */ import styled from '@emotion/styled'; +import FolderOffIcon from '@mui/icons-material/FolderOff'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import React, { useCallback } from 'react'; @@ -7,8 +9,6 @@ import { useHistory } from 'react-router-dom'; import { Theme } from '../../'; import Heading from '../Heading'; -// @ts-ignore -import PackageImg from './img/package.svg'; const NotFound: React.FC = () => { const history = useHistory(); @@ -28,7 +28,9 @@ const NotFound: React.FC = () => { justifyContent="center" p={2} > - + + + {t('error.404.sorry-we-could-not-find-it')} @@ -41,8 +43,7 @@ const NotFound: React.FC = () => { export default NotFound; -const EmptyPackage = styled('img')({ - width: '150px', +const Container = styled('div')({ margin: '0 auto', }); diff --git a/packages/ui-components/src/components/NotFound/Notfound.test.tsx b/packages/ui-components/src/components/NotFound/Notfound.test.tsx index 85cabd383..6c7278396 100644 --- a/packages/ui-components/src/components/NotFound/Notfound.test.tsx +++ b/packages/ui-components/src/components/NotFound/Notfound.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router'; -import { fireEvent, render } from '../../test/test-react-testing-library'; +import { fireEvent, render, screen } from '../../test/test-react-testing-library'; import NotFound from './NotFound'; const mockHistory = jest.fn(); @@ -14,12 +14,15 @@ jest.mock('react-router-dom', () => ({ describe(' component', () => { test('should load the component in default state', () => { - const { container } = render( + render( ); - expect(container.firstChild).toMatchSnapshot(); + + expect(screen.getByTestId('FolderOffIcon')).toBeInTheDocument(); + expect(screen.getByText('button.go-to-the-home-page')).toBeInTheDocument(); + expect(screen.getByText('error.404.sorry-we-could-not-find-it')).toBeInTheDocument(); }); test('go to Home Page button click', async () => { diff --git a/packages/ui-components/src/components/NotFound/__snapshots__/Notfound.test.tsx.snap b/packages/ui-components/src/components/NotFound/__snapshots__/Notfound.test.tsx.snap deleted file mode 100644 index 7804bc84e..000000000 --- a/packages/ui-components/src/components/NotFound/__snapshots__/Notfound.test.tsx.snap +++ /dev/null @@ -1,171 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` component should load the component in default state 1`] = ` -.emotion-0 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; - padding: 16px; -} - -.emotion-1 { - width: 150px; - margin: 0 auto; -} - -.emotion-4 { - margin: 0; - font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; - font-weight: 400; - font-size: 2.125rem; - line-height: 1.235; - color: #4b5e40; - margin-bottom: 16px; -} - -.emotion-5 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; - position: relative; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; - background-color: transparent; - outline: 0; - border: 0; - margin: 0; - border-radius: 0; - padding: 0; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - vertical-align: middle; - -moz-appearance: none; - -webkit-appearance: none; - -webkit-text-decoration: none; - text-decoration: none; - color: inherit; - font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; - font-weight: 500; - font-size: 0.875rem; - line-height: 1.75; - text-transform: uppercase; - min-width: 64px; - padding: 6px 16px; - border-radius: 4px; - -webkit-transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - color: #fff; - background-color: #4b5e40; - box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12); -} - -.emotion-5::-moz-focus-inner { - border-style: none; -} - -.emotion-5.Mui-disabled { - pointer-events: none; - cursor: default; -} - -@media print { - .emotion-5 { - -webkit-print-color-adjust: exact; - color-adjust: exact; - } -} - -.emotion-5:hover { - -webkit-text-decoration: none; - text-decoration: none; - background-color: rgb(52, 65, 44); - box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12); -} - -@media (hover: none) { - .emotion-5:hover { - background-color: #4b5e40; - } -} - -.emotion-5:active { - box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12); -} - -.emotion-5.Mui-focusVisible { - box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12); -} - -.emotion-5.Mui-disabled { - color: rgba(0, 0, 0, 0.26); - box-shadow: none; - background-color: rgba(0, 0, 0, 0.12); -} - -.emotion-6 { - overflow: hidden; - pointer-events: none; - position: absolute; - z-index: 0; - top: 0; - right: 0; - bottom: 0; - left: 0; - border-radius: inherit; -} - -
- error.404.page-not-found -

- error.404.sorry-we-could-not-find-it -

- -
-`; diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts index 085636b6a..d113a369b 100644 --- a/packages/ui-components/src/index.ts +++ b/packages/ui-components/src/index.ts @@ -22,6 +22,7 @@ export { default as Label } from './components/Label'; export { default as Logo } from './components/Logo'; export { default as MenuItem } from './components/MenuItem'; export { default as NotFound } from './components/NotFound'; +export { default as Forbidden } from './components/Forbidden'; export { default as LoginDialog } from './components/LoginDialog'; export { default as Search } from './components/Search'; export { default as HeaderInfoDialog } from './components/HeaderInfoDialog'; diff --git a/packages/ui-components/src/sections/Detail/Detail.stories.tsx b/packages/ui-components/src/sections/Detail/Detail.stories.tsx index 648c7ad1f..7d3cc86eb 100644 --- a/packages/ui-components/src/sections/Detail/Detail.stories.tsx +++ b/packages/ui-components/src/sections/Detail/Detail.stories.tsx @@ -27,3 +27,13 @@ export const DetailJquery: any = () => ( ); + +export const DetailForbidden: any = () => ( + + + + + + + +); diff --git a/packages/ui-components/src/store/api.test.ts b/packages/ui-components/src/store/api.test.ts index 3db3fbad2..03019e75f 100644 --- a/packages/ui-components/src/store/api.test.ts +++ b/packages/ui-components/src/store/api.test.ts @@ -14,7 +14,8 @@ describe('api', () => { const handled = await handleResponseType(response); - expect(handled).toEqual([ok, responseText]); + expect(handled[0]).toBeFalsy(); + expect(handled[1]).toBeDefined(); }); test('should test tgz scenario', async () => { @@ -123,7 +124,7 @@ describe('api', () => { }) ); - await expect(api.request('/resource')).rejects.toThrow(new Error('something went wrong')); + await expect(api.request('/resource')).rejects.toThrow(new Error('Unknown error')); }); test('when api returns an error 5.x.x', async () => { diff --git a/packages/ui-components/src/store/api.ts b/packages/ui-components/src/store/api.ts index 4f322fc6d..85c26f3b2 100644 --- a/packages/ui-components/src/store/api.ts +++ b/packages/ui-components/src/store/api.ts @@ -1,5 +1,10 @@ import storage from './storage'; +class CustomError extends Error { + // @ts-ignore + code: number; +} + /** * Handles response according to content type * @param {object} response @@ -25,7 +30,8 @@ export function handleResponseType(response: Response): Promise<[boolean, any]> } } - return Promise.all([response.ok, response.text()]); + // error handling + return Promise.all([response.ok, response]); } const AuthHeader = 'Authorization'; @@ -55,12 +61,13 @@ class API { }) .then(handleResponseType) .then((response) => { - if (response[0]) { - resolve(response[1]); + const [ok, data] = response; + if (ok === true) { + resolve(data); } else { - // eslint-disable-next-line no-console - console.error(response); - reject(new Error('something went wrong')); + const error = new CustomError(data?.statusText ?? 'Unknown error'); + error.code = data?.status ?? 500; + reject(error); } }) .catch((error) => { diff --git a/packages/ui-components/src/store/models/manifest.ts b/packages/ui-components/src/store/models/manifest.ts index 9914ca4c8..80f54094e 100644 --- a/packages/ui-components/src/store/models/manifest.ts +++ b/packages/ui-components/src/store/models/manifest.ts @@ -33,6 +33,18 @@ export const manifest = createModel()({ return { ...state, hasNotBeenFound: true, + forbidden: false, + manifest: undefined, + packageName: undefined, + packageVersion: undefined, + readme: undefined, + }; + }, + forbidden(state) { + return { + ...state, + forbidden: true, + hasNotBeenFound: false, manifest: undefined, packageName: undefined, packageVersion: undefined, @@ -50,6 +62,7 @@ export const manifest = createModel()({ ...state, isError: true, hasNotBeenFound: false, + forbidden: false, manifest: undefined, packageName: undefined, packageVersion: undefined, @@ -64,6 +77,7 @@ export const manifest = createModel()({ packageVersion, readme, hasNotBeenFound: false, + forbidden: false, }; }, }, @@ -91,7 +105,11 @@ export const manifest = createModel()({ ); dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme }); } catch (error: any) { - dispatch.manifest.notFound(); + if (error.code === 404) { + dispatch.manifest.notFound(); + } else { + dispatch.manifest.forbidden(); + } } }, }),