From 0f84de85898e134efac3dc7500e580e15c7da25c Mon Sep 17 00:00:00 2001
From: weareoutman <wangshenwei@qq.com>
Date: Thu, 9 Jan 2025 18:34:44 +0800
Subject: [PATCH 1/2] feat(): support block pages by license blacklist

---
 bricks/e2e/src/list-by-use-brick/index.tsx    | 10 +--
 etc/runtime.api.md                            |  5 +-
 packages/brick-container/package.json         |  2 +-
 packages/easyops-runtime/package.json         |  2 +-
 packages/easyops-runtime/src/auth-v2.ts       | 10 ++-
 packages/easyops-runtime/src/auth.spec.ts     | 62 +++++++++++++++++-
 packages/easyops-runtime/src/auth.ts          | 37 +++++++++++
 .../src/menu/fetchMenuById.spec.ts            | 19 +++++-
 .../easyops-runtime/src/menu/fetchMenuById.ts | 55 ++++++++++------
 .../easyops-runtime/src/menu/interfaces.ts    |  4 +-
 packages/runtime/package.json                 |  2 +-
 .../runtime/src/internal/ErrorNode.spec.ts    |  8 +--
 packages/runtime/src/internal/ErrorNode.ts    | 28 ++++----
 packages/runtime/src/internal/Router.ts       | 17 +++--
 packages/runtime/src/internal/Runtime.spec.ts | 64 +++++++++++++++++++
 packages/runtime/src/internal/Runtime.ts      |  3 +-
 packages/runtime/src/internal/i18n.ts         |  4 ++
 yarn.lock                                     | 10 +--
 18 files changed, 282 insertions(+), 60 deletions(-)

diff --git a/bricks/e2e/src/list-by-use-brick/index.tsx b/bricks/e2e/src/list-by-use-brick/index.tsx
index 3ee68cf3a5..4eddea0e42 100644
--- a/bricks/e2e/src/list-by-use-brick/index.tsx
+++ b/bricks/e2e/src/list-by-use-brick/index.tsx
@@ -1,8 +1,8 @@
 import React from "react";
 import { createDecorators } from "@next-core/element";
 import { ReactNextElement } from "@next-core/react-element";
-import { ReactUseBrick } from "@next-core/react-runtime";
-import { UseSingleBrickConf } from "@next-core/types";
+import { ReactUseMultipleBricks } from "@next-core/react-runtime";
+import { UseBrickConf } from "@next-core/types";
 
 const { defineElement, property } = createDecorators();
 
@@ -13,7 +13,7 @@ export
 })
 class ListByUseBrick extends ReactNextElement {
   @property({ attribute: false })
-  accessor useBrick: UseSingleBrickConf;
+  accessor useBrick: UseBrickConf;
 
   @property({ attribute: false })
   accessor data: unknown;
@@ -29,7 +29,7 @@ export function ListByUseBrickComponent({
   useBrick,
   data,
 }: {
-  useBrick: UseSingleBrickConf;
+  useBrick: UseBrickConf;
   data: unknown;
 }) {
   if (!useBrick || !Array.isArray(data)) {
@@ -38,7 +38,7 @@ export function ListByUseBrickComponent({
   return (
     <>
       {data.map((datum, index) => (
-        <ReactUseBrick useBrick={useBrick} data={datum} key={index} />
+        <ReactUseMultipleBricks useBrick={useBrick} data={datum} key={index} />
       ))}
     </>
   );
diff --git a/etc/runtime.api.md b/etc/runtime.api.md
index a13f1e9386..912fddd4da 100644
--- a/etc/runtime.api.md
+++ b/etc/runtime.api.md
@@ -348,7 +348,7 @@ export interface PageViewInfo {
     // (undocumented)
     path?: string;
     // (undocumented)
-    status: "ok" | "failed" | "redirected" | "not-found";
+    status: "ok" | "failed" | "redirected" | "not-found" | "blocked";
 }
 
 // @public (undocumented)
@@ -441,6 +441,7 @@ export interface RuntimeHooks {
         isLoggedIn(): boolean;
         authenticate?(...args: unknown[]): unknown;
         logout?(...args: unknown[]): unknown;
+        isBlockedPath?(pathname: string): boolean;
     };
     // (undocumented)
     checkInstalledApps?: {
@@ -586,7 +587,7 @@ function updateTemplatePreviewSettings(appId: string, templateId: string, settin
 // dist/types/Dialog.d.ts:10:5 - (ae-forgotten-export) The symbol "show_2" needs to be exported by the entry point index.d.ts
 // dist/types/Notification.d.ts:8:5 - (ae-forgotten-export) The symbol "show" needs to be exported by the entry point index.d.ts
 // dist/types/StoryboardFunctionRegistry.d.ts:48:5 - (ae-forgotten-export) The symbol "FunctionCoverageSettings" needs to be exported by the entry point index.d.ts
-// dist/types/internal/Runtime.d.ts:34:9 - (ae-forgotten-export) The symbol "AppForCheck" needs to be exported by the entry point index.d.ts
+// dist/types/internal/Runtime.d.ts:35:9 - (ae-forgotten-export) The symbol "AppForCheck" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
 
diff --git a/packages/brick-container/package.json b/packages/brick-container/package.json
index e131cf0fea..dfbeff7d06 100644
--- a/packages/brick-container/package.json
+++ b/packages/brick-container/package.json
@@ -50,7 +50,7 @@
     "ws": "^8.18.0"
   },
   "devDependencies": {
-    "@next-api-sdk/api-gateway-sdk": "^1.1.0",
+    "@next-api-sdk/api-gateway-sdk": "^1.2.2",
     "@next-api-sdk/micro-app-standalone-sdk": "^1.1.0",
     "@next-core/build-next-bricks": "^1.23.7",
     "@next-core/easyops-runtime": "^0.12.46",
diff --git a/packages/easyops-runtime/package.json b/packages/easyops-runtime/package.json
index d3bf2be7cc..520d186409 100644
--- a/packages/easyops-runtime/package.json
+++ b/packages/easyops-runtime/package.json
@@ -41,7 +41,7 @@
     "test:ci": "cross-env NODE_ENV='test' CI=true test-next"
   },
   "dependencies": {
-    "@next-api-sdk/api-gateway-sdk": "^1.2.0",
+    "@next-api-sdk/api-gateway-sdk": "^1.2.2",
     "@next-api-sdk/cmdb-sdk": "^1.1.0",
     "@next-api-sdk/micro-app-sdk": "^1.2.1",
     "@next-api-sdk/micro-app-standalone-sdk": "^1.1.0",
diff --git a/packages/easyops-runtime/src/auth-v2.ts b/packages/easyops-runtime/src/auth-v2.ts
index 5afc06d93c..74bd8e5cb8 100644
--- a/packages/easyops-runtime/src/auth-v2.ts
+++ b/packages/easyops-runtime/src/auth-v2.ts
@@ -7,7 +7,15 @@ export function authV2Factory() {
   const v2Kit = getV2RuntimeFromDll();
   if (v2Kit) {
     return Object.freeze(
-      pick(v2Kit, ["authenticate", "getAuth", "isLoggedIn", "logout"])
+      pick(v2Kit, [
+        "authenticate",
+        "getAuth",
+        "isLoggedIn",
+        "logout",
+        "isBlockedPath",
+        "isBlockedUrl",
+        "isBlockedHref",
+      ])
     ) as typeof auth;
   }
 }
diff --git a/packages/easyops-runtime/src/auth.spec.ts b/packages/easyops-runtime/src/auth.spec.ts
index b69f1ed4f4..357c321d2c 100644
--- a/packages/easyops-runtime/src/auth.spec.ts
+++ b/packages/easyops-runtime/src/auth.spec.ts
@@ -1,9 +1,26 @@
-import { authenticate, getAuth, logout, isLoggedIn } from "./auth.js";
+import {
+  authenticate,
+  getAuth,
+  logout,
+  isLoggedIn,
+  isBlockedPath,
+  isBlockedHref,
+  isBlockedUrl,
+} from "./auth.js";
 // import { resetPermissionPreChecks } from "./internal/checkPermissions.js";
 
 // jest.mock("./internal/checkPermissions");
 
 describe("auth", () => {
+  const base = document.createElement("base");
+  beforeAll(() => {
+    base.setAttribute("href", "/next/");
+    document.head.appendChild(base);
+  });
+  afterAll(() => {
+    document.head.removeChild(base);
+  });
+
   it("should work", () => {
     expect(getAuth()).toEqual({});
     expect(isLoggedIn()).toEqual(false);
@@ -12,15 +29,58 @@ describe("auth", () => {
       username: "mock-user",
       userInstanceId: "abc",
       accessRule: "cmdb",
+      license: {
+        blackList: ["/a", "/b/:id/c"],
+      },
     });
     expect(getAuth()).toEqual({
       org: 8888,
       username: "mock-user",
       userInstanceId: "abc",
       accessRule: "cmdb",
+      license: {
+        blackList: ["/a", "/b/:id/c"],
+      },
     });
     expect(isLoggedIn()).toEqual(true);
 
+    expect(isBlockedPath("/a")).toEqual(true);
+    expect(isBlockedPath("/a/123")).toEqual(true);
+    expect(isBlockedPath("/b")).toEqual(false);
+    expect(isBlockedPath("/b/123")).toEqual(false);
+    expect(isBlockedPath("/b/123/c")).toEqual(true);
+    expect(isBlockedPath("/b/123/c/d")).toEqual(true);
+    expect(isBlockedPath("/b/123/x")).toEqual(false);
+    expect(isBlockedPath("/c")).toEqual(false);
+
+    expect(isBlockedHref("/a")).toEqual(false);
+    expect(isBlockedHref("/next/a")).toEqual(true);
+    expect(isBlockedHref("a")).toEqual(true);
+    expect(isBlockedHref("http://localhost/a")).toEqual(false);
+    expect(isBlockedHref("http://localhost/next/a")).toEqual(true);
+    expect(isBlockedHref("http://example.com/a")).toEqual(false);
+    expect(isBlockedHref("http://example.com/next/a")).toEqual(false);
+
+    expect(isBlockedUrl("/a?q=1")).toEqual(true);
+    expect(isBlockedUrl("/next/a?q=1")).toEqual(false);
+    expect(
+      isBlockedUrl({
+        pathname: "/a",
+        search: "?q=1",
+      })
+    ).toEqual(true);
+    expect(
+      isBlockedUrl({
+        pathname: "/next/a",
+        search: "?q=1",
+      })
+    ).toEqual(false);
+    expect(
+      isBlockedUrl({
+        search: "?q=1",
+      })
+    ).toEqual(false);
+
     // expect(resetPermissionPreChecks).not.toBeCalled();
     logout();
     expect(getAuth()).toEqual({});
diff --git a/packages/easyops-runtime/src/auth.ts b/packages/easyops-runtime/src/auth.ts
index 97a423c28c..3b9a00dc54 100644
--- a/packages/easyops-runtime/src/auth.ts
+++ b/packages/easyops-runtime/src/auth.ts
@@ -1,4 +1,6 @@
+import { getBasePath, matchPath } from "@next-core/runtime";
 import type { AuthApi_CheckLoginResponseBody } from "@next-api-sdk/api-gateway-sdk";
+import { createLocation, type LocationDescriptor } from "history";
 import { resetPermissionPreChecks } from "./checkPermissions.js";
 
 const auth: AuthInfo = {};
@@ -49,3 +51,38 @@ export function logout(): void {
 export function isLoggedIn(): boolean {
   return auth.username !== undefined;
 }
+
+/**
+ * 判断一个内部 URL 路径是否被屏蔽。
+ */
+export function isBlockedPath(pathname: string): boolean {
+  return !!auth.license?.blackList?.some((path) =>
+    matchPath(pathname, { path })
+  );
+}
+
+/**
+ * 判断一个内部 URL 是否被屏蔽。
+ */
+export function isBlockedUrl(url: string | LocationDescriptor): boolean {
+  const pathname = (typeof url === "string" ? createLocation(url) : url)
+    .pathname;
+  if (typeof pathname !== "string") {
+    return false;
+  }
+  return isBlockedPath(pathname);
+}
+
+/**
+ * 判断一个 href 是否被屏蔽。
+ */
+export function isBlockedHref(href: string): boolean {
+  const basePath = getBasePath();
+  const url = new URL(href, `${location.origin}${basePath}`);
+  // 忽略外链地址
+  if (url.origin !== location.origin || !url.pathname.startsWith(basePath)) {
+    return false;
+  }
+  // 转换为内部路径
+  return isBlockedPath(url.pathname.substring(basePath.length - 1));
+}
diff --git a/packages/easyops-runtime/src/menu/fetchMenuById.spec.ts b/packages/easyops-runtime/src/menu/fetchMenuById.spec.ts
index 39a9daefc4..364959957e 100644
--- a/packages/easyops-runtime/src/menu/fetchMenuById.spec.ts
+++ b/packages/easyops-runtime/src/menu/fetchMenuById.spec.ts
@@ -9,6 +9,7 @@ import { InstalledMicroAppApi_getMenusInfo } from "@next-api-sdk/micro-app-sdk";
 import { createProviderClass } from "@next-core/utils/general";
 import { __test_only, createRuntime } from "@next-core/runtime";
 import { YAMLException } from "js-yaml";
+import type { LocationDescriptor } from "history";
 import { fetchMenuById, getMenuById } from "./fetchMenuById.js";
 import type { RuntimeContext, RuntimeHelpers } from "./interfaces.js";
 import * as auth from "../auth.js";
@@ -32,6 +33,14 @@ jest.mock("../auth.js", () => ({
   isAdmin() {
     return false;
   },
+  isBlockedUrl(url: LocationDescriptor) {
+    return typeof url === "string"
+      ? url.includes("blocked")
+      : url.pathname?.includes("blocked");
+  },
+  isBlockedHref(href: string) {
+    return href.includes("blocked");
+  },
 }));
 
 createRuntime({
@@ -91,6 +100,14 @@ const menuList = [
         text: "Menu Item 6",
         to: '/${ APP.unknown = ["next","test"] | join : "/" }',
       },
+      {
+        text: "Menu Item blocked by to",
+        to: "/to/blocked",
+      },
+      {
+        text: "Menu Item blocked by href",
+        href: "/href/blocked",
+      },
       {
         text: "Menu Item 7",
         children: [
@@ -211,7 +228,7 @@ const menuList = [
 
 (
   InstanceApi_postSearch as jest.Mock<typeof InstanceApi_postSearch>
-).mockImplementation(async (objectId, data: any) => {
+).mockImplementation(async (_objectId, data: any) => {
   return {
     list: menuList.filter((menu) => menu.menuId === data.query.menuId.$eq),
   };
diff --git a/packages/easyops-runtime/src/menu/fetchMenuById.ts b/packages/easyops-runtime/src/menu/fetchMenuById.ts
index 7712650f69..d2a622d82d 100644
--- a/packages/easyops-runtime/src/menu/fetchMenuById.ts
+++ b/packages/easyops-runtime/src/menu/fetchMenuById.ts
@@ -11,11 +11,13 @@ import type {
   RuntimeMenuItemRawData,
   SidebarMenu,
   SidebarMenuItem,
+  SidebarMenuSimpleItem,
 } from "./interfaces.js";
 import { computeMenuItems, computeMenuData } from "./computeMenuData.js";
 import { fetchMenuTitle } from "./fetchMenuTitle.js";
 import { getMenusOfStandaloneApp } from "./getMenusOfStandaloneApp.js";
 import { preCheckPermissionsForAny } from "../checkPermissions.js";
+import { isBlockedHref, isBlockedUrl } from "../auth.js";
 
 const menuPromises = new Map<string, Promise<void>>();
 
@@ -24,30 +26,43 @@ const menuCache = new Map<string, SidebarMenu>();
 function transformMenuItems(
   menuItems: RuntimeMenuItemRawData[] | undefined
 ): SidebarMenuItem[] | undefined {
-  return menuItems?.filter(checkIfOfComputed).map((item) => {
-    const children = transformMenuItems(item.children);
-    const transformedMenuItem: SidebarMenuItem =
-      item.type === "group"
-        ? {
-            type: "group",
-            title: item.text,
-            childLayout: item.childLayout,
-            items: children,
-            groupId: item.groupId,
-            groupFrom: item.groupFrom,
-          }
-        : children?.length
+  return menuItems
+    ?.filter(checkIfOfComputed)
+    .map((item) => {
+      const children = transformMenuItems(item.children);
+      const transformedMenuItem: SidebarMenuItem | null =
+        item.type === "group"
           ? {
-              type: "subMenu",
-              childLayout: item.childLayout,
+              type: "group",
               title: item.text,
-              icon: item.icon,
+              childLayout: item.childLayout,
               items: children,
-              defaultExpanded: item.defaultExpanded,
+              groupId: item.groupId,
+              groupFrom: item.groupFrom,
             }
-          : (omit(item, ["type", "items", "children"]) as SidebarMenuItem);
-    return transformedMenuItem;
-  });
+          : children?.length
+            ? {
+                type: "subMenu",
+                childLayout: item.childLayout,
+                title: item.text,
+                icon: item.icon,
+                items: children,
+                defaultExpanded: item.defaultExpanded,
+              }
+            : isMenuItemBlocked(item as SidebarMenuSimpleItem)
+              ? null
+              : (omit(item, ["type", "items", "children"]) as SidebarMenuItem);
+      return transformedMenuItem;
+    })
+    .filter(Boolean) as SidebarMenuItem[];
+}
+
+function isMenuItemBlocked(item: SidebarMenuSimpleItem) {
+  return item.href
+    ? isBlockedHref(item.href)
+    : item.to
+      ? isBlockedUrl(item.to)
+      : false;
 }
 
 export function getMenuById(menuId: string) {
diff --git a/packages/easyops-runtime/src/menu/interfaces.ts b/packages/easyops-runtime/src/menu/interfaces.ts
index 622f70ec52..b5b242dedd 100644
--- a/packages/easyops-runtime/src/menu/interfaces.ts
+++ b/packages/easyops-runtime/src/menu/interfaces.ts
@@ -4,6 +4,7 @@ import type {
   __secret_internals,
   MatchOptions,
 } from "@next-core/runtime";
+import type { LocationDescriptor } from "history";
 import {
   symbolAppId,
   symbolMenuI18nNamespace,
@@ -61,8 +62,7 @@ export interface SidebarMenuSimpleItem {
   text: string;
 
   /** 菜单项对应的系统内地址。 */
-  // to?: LocationDescriptor;
-  to?: unknown;
+  to?: LocationDescriptor;
 
   /** 菜单项对应的外部链接地址。 */
   href?: string;
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 7f48497d2c..f7fdabbb46 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -62,7 +62,7 @@
   },
   "devDependencies": {
     "@microsoft/api-extractor": "^7.47.9",
-    "@next-api-sdk/api-gateway-sdk": "^1.1.0",
+    "@next-api-sdk/api-gateway-sdk": "^1.2.2",
     "@next-api-sdk/micro-app-sdk": "^1.2.1",
     "@next-core/build-next-libs": "^1.0.21",
     "@next-core/test-next": "^1.1.7"
diff --git a/packages/runtime/src/internal/ErrorNode.spec.ts b/packages/runtime/src/internal/ErrorNode.spec.ts
index 6228848b6b..9fdb3959e2 100644
--- a/packages/runtime/src/internal/ErrorNode.spec.ts
+++ b/packages/runtime/src/internal/ErrorNode.spec.ts
@@ -1,7 +1,7 @@
 import { jest, describe, test, expect } from "@jest/globals";
 import { BrickLoadError, loadBricksImperatively } from "@next-core/loader";
 import { initializeI18n } from "@next-core/i18n";
-import { ErrorNode, PageNotFoundError } from "./ErrorNode.js";
+import { ErrorNode, PageError } from "./ErrorNode.js";
 import { RenderTag } from "./enums.js";
 import type { RenderReturnNode } from "./interfaces.js";
 import { HttpResponseError } from "@next-core/http";
@@ -262,7 +262,7 @@ describe("ErrorNode", () => {
   test("page not found", async () => {
     expect(
       await ErrorNode(
-        new PageNotFoundError("page not found"),
+        new PageError("page not found"),
         {
           tag: RenderTag.ROOT,
         } as RenderReturnNode,
@@ -297,7 +297,7 @@ describe("ErrorNode", () => {
   test("app not found", async () => {
     expect(
       await ErrorNode(
-        new PageNotFoundError("app not found"),
+        new PageError("app not found"),
         {
           tag: RenderTag.ROOT,
         } as RenderReturnNode,
@@ -336,7 +336,7 @@ describe("ErrorNode", () => {
 
     expect(
       await ErrorNode(
-        new PageNotFoundError("app not found"),
+        new PageError("app not found"),
         {
           tag: RenderTag.ROOT,
         } as RenderReturnNode,
diff --git a/packages/runtime/src/internal/ErrorNode.ts b/packages/runtime/src/internal/ErrorNode.ts
index e6c0b9a189..957a60b828 100644
--- a/packages/runtime/src/internal/ErrorNode.ts
+++ b/packages/runtime/src/internal/ErrorNode.ts
@@ -24,12 +24,12 @@ interface ErrorMessageConfig {
 
 type LinkType = "home" | "previous" | "reload";
 
-export class PageNotFoundError extends Error {
-  constructor(message: "page not found" | "app not found") {
+export class PageError extends Error {
+  constructor(message: "page blocked" | "page not found" | "app not found") {
     // Pass remaining arguments (including vendor specific ones) to parent constructor
     super(message);
 
-    this.name = "PageNotFoundError";
+    this.name = "PageError";
 
     // Maintains proper stack trace for where our error was thrown (only available on V8)
     // istanbul ignore else
@@ -182,18 +182,24 @@ function getLinkNode(
 }
 
 function getRefinedErrorConf(error: unknown): ErrorMessageConfig {
-  if (error instanceof PageNotFoundError) {
-    return error.message === "app not found"
+  if (error instanceof PageError) {
+    return error.message === "page blocked"
       ? {
           showLink: "home",
-          title: i18n.t(`${NS}:${K.APP_NOT_FOUND}`),
+          title: i18n.t(`${NS}:${K.LICENSE_BLOCKED}`),
           variant: "no-permission",
         }
-      : {
-          showLink: "home",
-          variant: "not-found",
-          title: i18n.t(`${NS}:${K.PAGE_NOT_FOUND}`),
-        };
+      : error.message === "app not found"
+        ? {
+            showLink: "home",
+            title: i18n.t(`${NS}:${K.APP_NOT_FOUND}`),
+            variant: "no-permission",
+          }
+        : {
+            showLink: "home",
+            variant: "not-found",
+            title: i18n.t(`${NS}:${K.PAGE_NOT_FOUND}`),
+          };
   }
 
   if (
diff --git a/packages/runtime/src/internal/Router.ts b/packages/runtime/src/internal/Router.ts
index f67a0c23e0..5cba5b8459 100644
--- a/packages/runtime/src/internal/Router.ts
+++ b/packages/runtime/src/internal/Router.ts
@@ -55,7 +55,7 @@ import { setUIVersion } from "../setUIVersion.js";
 import { setAppVariable } from "../setAppVariable.js";
 import { setWatermark } from "../setWatermark.js";
 import { clearMatchedRoutes } from "./routeMatchedMap.js";
-import { ErrorNode, PageNotFoundError } from "./ErrorNode.js";
+import { ErrorNode, PageError } from "./ErrorNode.js";
 import {
   resetReloadForError,
   shouldReloadForError,
@@ -277,6 +277,7 @@ export class Router {
 
   async #render(location: NextLocation, isBootstrap: boolean): Promise<void> {
     const renderId = (this.#renderId = uniqueId("render-id-"));
+    const blocked = hooks?.auth?.isBlockedPath?.(location.pathname);
 
     resetAllComputedMarks();
     clearResolveCache();
@@ -288,7 +289,9 @@ export class Router {
     // const renderStartTime = performance.now();
     const finishPageView = hooks?.pageView?.create();
 
-    const storyboard = matchStoryboard(this.#storyboards, location.pathname);
+    const storyboard = blocked
+      ? undefined
+      : matchStoryboard(this.#storyboards, location.pathname);
 
     const previousApp = this.#runtimeContext?.app;
     const currentAppId = storyboard?.app?.id;
@@ -576,7 +579,13 @@ export class Router {
     applyMode();
 
     const node = await ErrorNode(
-      new PageNotFoundError(currentApp ? "page not found" : "app not found"),
+      new PageError(
+        blocked
+          ? "page blocked"
+          : currentApp
+            ? "page not found"
+            : "app not found"
+      ),
       renderRoot,
       true
     );
@@ -586,7 +595,7 @@ export class Router {
 
     // Scroll to top after each rendering.
     window.scrollTo(0, 0);
-    finishPageView?.({ status: "not-found" });
+    finishPageView?.({ status: blocked ? "blocked" : "not-found" });
     devtoolsHookEmit("rendered");
   }
 }
diff --git a/packages/runtime/src/internal/Runtime.spec.ts b/packages/runtime/src/internal/Runtime.spec.ts
index 322ef917ea..2d6663b421 100644
--- a/packages/runtime/src/internal/Runtime.spec.ts
+++ b/packages/runtime/src/internal/Runtime.spec.ts
@@ -1688,6 +1688,70 @@ describe("Runtime", () => {
     expect(finishPageView).toHaveBeenNthCalledWith(2, { status: "not-found" });
   });
 
+  test("page blocked", async () => {
+    window.NO_AUTH_GUARD = false;
+    const finishPageView = jest.fn();
+    createRuntime({
+      hooks: {
+        auth: {
+          isLoggedIn() {
+            return true;
+          },
+          getAuth() {
+            return {};
+          },
+          isBlockedPath() {
+            return true;
+          },
+        },
+        pageView: {
+          create: jest.fn(() => finishPageView),
+        },
+      },
+    }).initialize({
+      storyboards: [
+        {
+          app: {
+            id: "blocked-app",
+            homepage: "/blocked-app",
+            name: "Blocked APP",
+          },
+          routes: [
+            {
+              path: "${APP.homepage}/blocked",
+              bricks: [{ brick: "div" }],
+            },
+          ],
+        },
+      ],
+      brickPackages: [],
+    });
+    getHistory().push("/blocked-app/blocked");
+    await getRuntime().bootstrap();
+    expect(document.body.children).toMatchInlineSnapshot(`
+      HTMLCollection [
+        <div
+          id="main-mount-point"
+        >
+          <illustrations.error-message
+            data-error-boundary=""
+          >
+            <eo-link>
+              Go back to home page
+            </eo-link>
+          </illustrations.error-message>
+        </div>,
+        <div
+          id="portal-mount-point"
+        />,
+      ]
+    `);
+    expect(finishPageView).toBeCalledTimes(1);
+    expect(finishPageView).toBeCalledWith({
+      status: "blocked",
+    });
+  });
+
   test("failed to bootstrap", async () => {
     consoleError.mockReturnValueOnce();
     const finishPageView = jest.fn();
diff --git a/packages/runtime/src/internal/Runtime.ts b/packages/runtime/src/internal/Runtime.ts
index 06ccf1b892..598aa7549a 100644
--- a/packages/runtime/src/internal/Runtime.ts
+++ b/packages/runtime/src/internal/Runtime.ts
@@ -45,7 +45,7 @@ export interface ImagesFactory {
 }
 
 export interface PageViewInfo {
-  status: "ok" | "failed" | "redirected" | "not-found";
+  status: "ok" | "failed" | "redirected" | "not-found" | "blocked";
   path?: string;
   pageTitle?: string;
 }
@@ -56,6 +56,7 @@ export interface RuntimeHooks {
     isLoggedIn(): boolean;
     authenticate?(...args: unknown[]): unknown;
     logout?(...args: unknown[]): unknown;
+    isBlockedPath?(pathname: string): boolean;
   };
   fulfilStoryboard?: (storyboard: RuntimeStoryboard) => Promise<void>;
   validatePermissions?: typeof PermissionApi_validatePermissions;
diff --git a/packages/runtime/src/internal/i18n.ts b/packages/runtime/src/internal/i18n.ts
index d11497643b..60d14a13e9 100644
--- a/packages/runtime/src/internal/i18n.ts
+++ b/packages/runtime/src/internal/i18n.ts
@@ -4,6 +4,7 @@ export enum K {
   LOGIN_TIMEOUT_MESSAGE = "LOGIN_TIMEOUT_MESSAGE",
   NETWORK_ERROR = "NETWORK_ERROR",
   LICENSE_EXPIRED = "LICENSE_EXPIRED",
+  LICENSE_BLOCKED = "LICENSE_BLOCKED",
   NO_PERMISSION = "NO_PERMISSION",
   PAGE_NOT_FOUND = "PAGE_NOT_FOUND",
   APP_NOT_FOUND = "APP_NOT_FOUND",
@@ -21,6 +22,8 @@ const en: Locale = {
   [K.NETWORK_ERROR]: "Network error, please check your network.",
   [K.LICENSE_EXPIRED]:
     "The license authorization has expired, please contact the platform administrator",
+  [K.LICENSE_BLOCKED]:
+    "The page is not authorized, please contact the platform administrator",
   [K.NO_PERMISSION]:
     "Unauthorized access, unable to retrieve the required resources for this page",
   [K.PAGE_NOT_FOUND]: "Page not found, please check the URL",
@@ -38,6 +41,7 @@ const zh: Locale = {
   [K.LOGIN_TIMEOUT_MESSAGE]: "您还未登录或登录信息已过期,现在重新登录?",
   [K.NETWORK_ERROR]: "网络错误,请检查您的网络连接。",
   [K.LICENSE_EXPIRED]: "License 授权失效,请联系平台管理员",
+  [K.LICENSE_BLOCKED]: "该页面未授权,请联系平台管理员",
   [K.NO_PERMISSION]: "没有权限,无法获取页面所需要的资源",
   [K.PAGE_NOT_FOUND]: "请求的页面未找到,请确认 URL 是否正确",
   [K.APP_NOT_FOUND]: "请求的微应用无法找到, 可能是 URL 错误或者无权限访问",
diff --git a/yarn.lock b/yarn.lock
index e29ff4e3e5..6871e4d5ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1968,12 +1968,12 @@
   resolved "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz#f29a55df17cb6e87cfbabce33ff6a14a9f85076d"
   integrity sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==
 
-"@next-api-sdk/api-gateway-sdk@^1.1.0", "@next-api-sdk/api-gateway-sdk@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.npmjs.org/@next-api-sdk/api-gateway-sdk/-/api-gateway-sdk-1.2.0.tgz#68a49bf50f76c626234ce53cb3606f7dbeb2ca97"
-  integrity sha512-HG6wSG2hoH+ZXEH/AUIsbhUwtzXQgWKU7usrFVVQsGmqIQscjRYMSlCqEAJixP1IG9vQ0D+kkkaeAoSnhX3nEg==
+"@next-api-sdk/api-gateway-sdk@^1.2.2":
+  version "1.2.2"
+  resolved "https://registry.npmjs.org/@next-api-sdk/api-gateway-sdk/-/api-gateway-sdk-1.2.2.tgz#867b2b93573379fcd8f2507876bc0ce83bf19858"
+  integrity sha512-q4XFL3xOAcG+qboXWKRfENiiN72dVIs0w9JBurVj5NwbgSsF3L4L1T+v1MJlBIp7hhEzWa60SwvUh0IiRneNqA==
   dependencies:
-    "@next-core/http" "^1.0.0"
+    "@next-core/http" "^1.2.6"
 
 "@next-api-sdk/cmdb-sdk@^1.1.0":
   version "1.1.0"

From 60fd41482725e8d6088f80010b38c6e5213f5aa1 Mon Sep 17 00:00:00 2001
From: weareoutman <wangshenwei@qq.com>
Date: Fri, 10 Jan 2025 10:34:15 +0800
Subject: [PATCH 2/2] fix(): sync dep versions in babel config

---
 packages/babel-preset-next/index.js | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/packages/babel-preset-next/index.js b/packages/babel-preset-next/index.js
index eab6663ef9..0412955d2b 100644
--- a/packages/babel-preset-next/index.js
+++ b/packages/babel-preset-next/index.js
@@ -19,15 +19,14 @@ module.exports = () => {
           : env === "commonjs"
             ? {
                 targets: {
-                  // Fallback to versions that doesn't support nullish coalescing
-                  node: "12",
+                  node: "14",
                 },
               }
             : {
                 modules: false,
                 useBuiltIns: "entry",
                 corejs: {
-                  version: "3.37",
+                  version: "3.38",
                 },
               },
       ],
@@ -50,7 +49,7 @@ module.exports = () => {
         transformRuntime,
         {
           // https://github.com/babel/babel/issues/9454#issuecomment-460425922
-          version: "7.24.5",
+          version: "7.25.6",
         },
       ],
     ],