Просмотр исходного кода

Merge branch 'master' into fix/156012-156299-use-all-emoji

Yuki Takei 1 год назад
Родитель
Сommit
011786dfca
99 измененных файлов с 400 добавлено и 449 удалено
  1. 0 1
      .changeset/config.json
  2. 1 2
      .github/workflows/reusable-app-prod.yml
  3. 2 1
      .vscode/launch.json
  4. 5 2
      apps/app/next.config.js
  5. 4 1
      apps/app/package.json
  6. 16 0
      apps/app/src/features/openai/server/models/vector-store-file-relation.ts
  7. 1 1
      apps/app/src/features/openai/server/routes/message.ts
  8. 1 1
      apps/app/src/features/openai/server/routes/rebuild-vector-store.ts
  9. 1 1
      apps/app/src/features/openai/server/routes/thread.ts
  10. 0 0
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.spec.ts
  11. 14 7
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.ts
  12. 0 0
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.spec.ts
  13. 2 2
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.ts
  14. 23 10
      apps/app/src/features/openai/server/services/openai.ts
  15. 8 7
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  16. 1 1
      apps/app/src/migrations/20180926134048-make-email-unique.js
  17. 2 3
      apps/app/src/migrations/20180927102719-init-serverurl.js
  18. 2 2
      apps/app/src/migrations/20181019114028-abolish-page-group-relation.js
  19. 1 1
      apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  20. 2 2
      apps/app/src/migrations/20190618104011-add-config-app-installed.js
  21. 1 1
      apps/app/src/migrations/20190619055421-adjust-page-grant.js
  22. 1 1
      apps/app/src/migrations/20190624110950-fill-last-update-user.js
  23. 1 1
      apps/app/src/migrations/20190629193445-make-root-page-public.js
  24. 1 1
      apps/app/src/migrations/20191102223900-drop-configs-indices.js
  25. 1 1
      apps/app/src/migrations/20191102223901-drop-pages-indices.js
  26. 1 1
      apps/app/src/migrations/20191126173016-adjust-pages-path.js
  27. 1 1
      apps/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js
  28. 1 1
      apps/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js
  29. 1 1
      apps/app/src/migrations/20200420160390-remove-crowi-layout.js
  30. 2 2
      apps/app/src/migrations/20200512005851-remove-behavior-type.js
  31. 1 1
      apps/app/src/migrations/20200514001356-update-theme-color-for-dark.js
  32. 1 1
      apps/app/src/migrations/20200620203632-normalize-locale-id.js
  33. 2 2
      apps/app/src/migrations/20200827045151-remove-layout-setting.js
  34. 2 2
      apps/app/src/migrations/20200828024025-copy-aws-setting.js
  35. 2 2
      apps/app/src/migrations/20200901034313-update-mail-transmission.js
  36. 2 2
      apps/app/src/migrations/20200903080025-remove-timeline-type.js.js
  37. 2 2
      apps/app/src/migrations/20200915035234-rename-s3-config.js
  38. 1 1
      apps/app/src/migrations/20210420160380-convert-double-to-date.js
  39. 2 2
      apps/app/src/migrations/20210830074539-update-configs-for-slackbot.js
  40. 1 1
      apps/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js
  41. 2 2
      apps/app/src/migrations/20210921173042-add-is-trashed-field.js
  42. 2 2
      apps/app/src/migrations/20211005120030-slack-app-integration-rename-keys.js
  43. 2 2
      apps/app/src/migrations/20211005131430-config-without-proxy-command-permission-for-renaming.js
  44. 2 2
      apps/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js
  45. 2 2
      apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js
  46. 2 2
      apps/app/src/migrations/20220131001218-convert-redirect-to-pages-to-page-redirect-documents.js
  47. 1 1
      apps/app/src/migrations/20220311011114-convert-page-delete-config.js
  48. 1 1
      apps/app/src/migrations/20220411114257-set-sparse-option-to-slack-member-id.js
  49. 1 1
      apps/app/src/migrations/20220613064207-add-attachment-type-to-existing-attachments.js
  50. 2 2
      apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js
  51. 1 1
      apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  52. 2 2
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  53. 2 2
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  54. 1 1
      apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js
  55. 9 0
      apps/app/src/server/crowi/index.js
  56. 0 37
      apps/app/src/server/middlewares/access-token-parser.js
  57. 135 0
      apps/app/src/server/middlewares/access-token-parser/access-token-parser.integ.ts
  58. 38 0
      apps/app/src/server/middlewares/access-token-parser/access-token-parser.ts
  59. 1 0
      apps/app/src/server/middlewares/access-token-parser/index.ts
  60. 14 0
      apps/app/src/server/middlewares/access-token-parser/interfaces.ts
  61. 5 11
      apps/app/src/server/models/user.js
  62. 1 1
      apps/app/src/server/routes/apiv3/activity.ts
  63. 1 1
      apps/app/src/server/routes/apiv3/app-settings.js
  64. 1 1
      apps/app/src/server/routes/apiv3/attachment.js
  65. 1 1
      apps/app/src/server/routes/apiv3/bookmark-folder.ts
  66. 1 1
      apps/app/src/server/routes/apiv3/bookmarks.js
  67. 1 1
      apps/app/src/server/routes/apiv3/export.js
  68. 1 1
      apps/app/src/server/routes/apiv3/g2g-transfer.ts
  69. 1 1
      apps/app/src/server/routes/apiv3/import.js
  70. 1 1
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  71. 1 1
      apps/app/src/server/routes/apiv3/page-listing.ts
  72. 2 2
      apps/app/src/server/routes/apiv3/page/check-page-existence.ts
  73. 1 1
      apps/app/src/server/routes/apiv3/page/create-page.ts
  74. 1 1
      apps/app/src/server/routes/apiv3/page/get-yjs-data.ts
  75. 1 1
      apps/app/src/server/routes/apiv3/page/index.ts
  76. 1 1
      apps/app/src/server/routes/apiv3/page/publish-page.ts
  77. 1 1
      apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts
  78. 1 1
      apps/app/src/server/routes/apiv3/page/unpublish-page.ts
  79. 1 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  80. 1 1
      apps/app/src/server/routes/apiv3/pages/index.js
  81. 1 1
      apps/app/src/server/routes/apiv3/personal-setting.js
  82. 1 1
      apps/app/src/server/routes/apiv3/revisions.js
  83. 1 1
      apps/app/src/server/routes/apiv3/search.js
  84. 1 1
      apps/app/src/server/routes/apiv3/slack-integration-settings.js
  85. 1 1
      apps/app/src/server/routes/apiv3/users.js
  86. 1 1
      apps/app/src/server/routes/index.js
  87. 1 1
      apps/app/test/integration/global-setup.js
  88. 0 75
      apps/app/test/integration/middlewares/access-token-parser.test.js
  89. 0 2
      packages/markdown-splitter/.eslintignore
  90. 0 5
      packages/markdown-splitter/.eslintrc.cjs
  91. 0 1
      packages/markdown-splitter/.gitignore
  92. 0 49
      packages/markdown-splitter/package.json
  93. 0 2
      packages/markdown-splitter/src/index.ts
  94. 0 16
      packages/markdown-splitter/tsconfig.json
  95. 0 39
      packages/markdown-splitter/vite.config.ts
  96. 0 25
      packages/markdown-splitter/vitest.config.ts
  97. 1 1
      packages/remark-attachment-refs/src/server/routes/refs.ts
  98. 9 3
      packages/remark-lsx/src/server/index.ts
  99. 25 61
      pnpm-lock.yaml

+ 0 - 1
.changeset/config.json

@@ -15,7 +15,6 @@
     "@growi/app",
     "@growi/slackbot-proxy",
     "@growi/custom-icons",
-    "@growi/markdown-splitter",
     "@growi/editor",
     "@growi/presentation",
     "@growi/preset-templates",

+ 1 - 2
.github/workflows/reusable-app-prod.yml

@@ -95,8 +95,7 @@ jobs:
       with:
         name: Bundle Analyzing Report (node${{ inputs.node-version }})
         path: |
-          apps/app/.next/analyze/client.html
-          apps/app/.next/analyze/server.html
+          apps/app/.next/analyze
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master

+ 2 - 1
.vscode/launch.json

@@ -31,8 +31,9 @@
         "request": "launch",
         "name": "Debug: Server",
         "cwd": "${workspaceFolder}/apps/app",
-        "runtimeExecutable": "yarn",
+        "runtimeExecutable": "pnpm",
         "runtimeArgs": [
+          "run",
           "dev"
         ],
         "skipFiles": [

+ 5 - 2
apps/app/next.config.js

@@ -73,7 +73,6 @@ const getTranspilePackages = () => {
 const optimizePackageImports = [
   '@growi/core',
   '@growi/editor',
-  '@growi/markdown-splitter',
   '@growi/pluginkit',
   '@growi/presentation',
   '@growi/preset-themes',
@@ -160,7 +159,11 @@ module.exports = async(phase, { defaultConfig }) => {
   }
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
-    enabled: phase === PHASE_PRODUCTION_BUILD && process.env.ANALYZE === 'true',
+    enabled: phase === PHASE_PRODUCTION_BUILD
+      && (
+        process.env.ANALYZE === 'true'
+          || process.env.ANALYZE === '1'
+      ),
   });
 
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 4 - 1
apps/app/package.json

@@ -67,6 +67,7 @@
     "@azure/openai": "^2.0.0-beta.2",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
+    "@cspell/dynamic-import": "^8.15.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
@@ -128,6 +129,8 @@
     "i18next-resources-to-backend": "^1.2.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
+    "js-tiktoken": "^1.0.15",
+    "js-yaml": "^4.1.0",
     "katex": "^0.16.11",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
@@ -201,6 +204,7 @@
     "remark-math": "^6.0.0",
     "remark-parse": "^11.0.0",
     "remark-rehype": "^11.1.1",
+    "remark-stringify": "^11.0.0",
     "remark-toc": "^9.0.0",
     "sanitize-filename": "^1.6.3",
     "socket.io": "^4.7.5",
@@ -237,7 +241,6 @@
     "@growi/core-styles": "workspace:^",
     "@growi/custom-icons": "workspace:^",
     "@growi/editor": "workspace:^",
-    "@growi/markdown-splitter": "workspace:^",
     "@growi/ui": "workspace:^",
     "@handsontable/react": "=2.1.0",
     "@next/bundle-analyzer": "^14.1.3",

+ 16 - 0
apps/app/src/features/openai/server/models/vector-store-file-relation.ts

@@ -7,12 +7,14 @@ import { getOrCreateModel } from '~/server/util/mongoose-utils';
 export interface VectorStoreFileRelation {
   pageId: mongoose.Types.ObjectId;
   fileIds: string[];
+  isAttachedToVectorStore: boolean;
 }
 
 interface VectorStoreFileRelationDocument extends VectorStoreFileRelation, Document {}
 
 interface VectorStoreFileRelationModel extends Model<VectorStoreFileRelation> {
   upsertVectorStoreFileRelations(vectorStoreFileRelations: VectorStoreFileRelation[]): Promise<void>;
+  markAsAttachedToVectorStore(pageIds: Types.ObjectId[]): Promise<void>;
 }
 
 export const prepareVectorStoreFileRelations = (
@@ -30,6 +32,7 @@ export const prepareVectorStoreFileRelations = (
     relationsMap.set(pageIdStr, {
       pageId,
       fileIds: [fileId],
+      isAttachedToVectorStore: false,
     });
   }
 
@@ -47,6 +50,11 @@ const schema = new Schema<VectorStoreFileRelationDocument, VectorStoreFileRelati
     type: String,
     required: true,
   }],
+  isAttachedToVectorStore: {
+    type: Boolean,
+    default: false, // File is not attached to the Vector Store at the time it is uploaded
+    required: true,
+  },
 });
 
 schema.statics.upsertVectorStoreFileRelations = async function(vectorStoreFileRelations: VectorStoreFileRelation[]): Promise<void> {
@@ -63,4 +71,12 @@ schema.statics.upsertVectorStoreFileRelations = async function(vectorStoreFileRe
   );
 };
 
+// Used when attached to VectorStore
+schema.statics.markAsAttachedToVectorStore = async function(pageIds: Types.ObjectId[]): Promise<void> {
+  await this.updateMany(
+    { pageId: { $in: pageIds } },
+    { $set: { isAttachedToVectorStore: true } },
+  );
+};
+
 export default getOrCreateModel<VectorStoreFileRelationDocument, VectorStoreFileRelationModel>('VectorStoreFileRelation', schema);

+ 1 - 1
apps/app/src/features/openai/server/routes/message.ts

@@ -7,6 +7,7 @@ import type { MessageDelta } from 'openai/resources/beta/threads/messages.mjs';
 
 import { getOrCreateChatAssistant } from '~/features/openai/server/services/assistant';
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
@@ -30,7 +31,6 @@ type Req = Request<undefined, Response, ReqBody>
 type PostMessageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
 export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [

+ 1 - 1
apps/app/src/features/openai/server/routes/rebuild-vector-store.ts

@@ -3,6 +3,7 @@ import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
@@ -16,7 +17,6 @@ const logger = loggerFactory('growi:routes:apiv3:openai:rebuild-vector-store');
 type RebuildVectorStoreFactory = (crowi: Crowi) => RequestHandler[];
 
 export const rebuildVectorStoreHandlersFactory: RebuildVectorStoreFactory = (crowi) => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
 

+ 1 - 1
apps/app/src/features/openai/server/routes/thread.ts

@@ -5,6 +5,7 @@ import { body } from 'express-validator';
 import { filterXSS } from 'xss';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
@@ -20,7 +21,6 @@ type CreateThreadReq = Request<undefined, ApiV3Response, { threadId?: string }>
 type CreateThreadFactory = (crowi: Crowi) => RequestHandler[];
 
 export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [

+ 0 - 0
packages/markdown-splitter/src/services/markdown-splitter.spec.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.spec.ts


+ 14 - 7
packages/markdown-splitter/src/services/markdown-splitter.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.ts

@@ -1,12 +1,13 @@
+import { dynamicImport } from '@cspell/dynamic-import';
 import type { TiktokenModel } from 'js-tiktoken';
 import { encodingForModel } from 'js-tiktoken';
 import yaml from 'js-yaml';
-import remarkFrontmatter from 'remark-frontmatter'; // Frontmatter processing
-import remarkGfm from 'remark-gfm'; // GFM processing
-import remarkParse from 'remark-parse';
-import type { Options as StringifyOptions } from 'remark-stringify';
-import remarkStringify from 'remark-stringify';
-import { unified } from 'unified';
+import type * as RemarkFrontmatter from 'remark-frontmatter';
+import type * as RemarkGfm from 'remark-gfm';
+import type * as RemarkParse from 'remark-parse';
+import type * as RemarkStringify from 'remark-stringify';
+import type * as Unified from 'unified';
+
 
 export type MarkdownFragment = {
   label: string;
@@ -59,12 +60,18 @@ export async function splitMarkdownIntoFragments(markdownText: string, model: Ti
 
   const encoder = encodingForModel(model);
 
+  const remarkParse = (await dynamicImport<typeof RemarkParse>('remark-parse', __dirname)).default;
+  const remarkFrontmatter = (await dynamicImport<typeof RemarkFrontmatter>('remark-frontmatter', __dirname)).default;
+  const remarkGfm = (await dynamicImport<typeof RemarkGfm>('remark-gfm', __dirname)).default;
+  const remarkStringify = (await dynamicImport<typeof RemarkStringify>('remark-stringify', __dirname)).default;
+  const unified = (await dynamicImport<typeof Unified>('unified', __dirname)).unified;
+
   const parser = unified()
     .use(remarkParse)
     .use(remarkFrontmatter, ['yaml'])
     .use(remarkGfm); // Enable GFM extensions
 
-  const stringifyOptions: StringifyOptions = {
+  const stringifyOptions: RemarkStringify.Options = {
     bullet: '-', // Set list bullet to hyphen
     rule: '-', // Use hyphen for horizontal rules
   };

+ 0 - 0
packages/markdown-splitter/src/services/markdown-token-splitter.spec.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.spec.ts


+ 2 - 2
packages/markdown-splitter/src/services/markdown-token-splitter.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.ts

@@ -105,7 +105,7 @@ export async function splitMarkdownIntoChunks(
 
   // Split markdown text into chunks
   const markdownFragments = await splitMarkdownIntoFragments(markdownText, model);
-  const chunks = [] as string[];
+  const chunks: string[] = [];
 
   // Group the chunks based on token count
   const fragmentGroupes = groupMarkdownFragments(markdownFragments, maxToken);
@@ -162,7 +162,7 @@ export async function splitMarkdownIntoChunks(
             const charCountForSplit = Math.floor((remainingTokenCount / fragmenTokenCount) * fragmentCharCount);
 
             // Split content based on character count
-            const splitContents = [];
+            const splitContents: string[] = [];
             for (let i = 0; i < fragment.text.length; i += charCountForSplit) {
               splitContents.push(fragment.text.slice(i, i + charCountForSplit));
             }

+ 23 - 10
apps/app/src/features/openai/server/services/openai.ts

@@ -21,6 +21,7 @@ import loggerFactory from '~/utils/logger';
 import { OpenaiServiceTypes } from '../../interfaces/ai';
 
 import { getClient } from './client-delegator';
+import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
 import { oepnaiApiErrorHandler } from './openai-api-error-handler';
 
 const BATCH_SIZE = 100;
@@ -29,6 +30,8 @@ const logger = loggerFactory('growi:service:openai');
 
 let isVectorStoreForPublicScopeExist = false;
 
+type VectorStoreFileRelationsMap = Map<string, VectorStoreFileRelation>
+
 export interface IOpenaiService {
   getOrCreateThread(userId: string, vectorStoreId?: string, threadId?: string): Promise<OpenAI.Beta.Threads.Thread | undefined>;
   getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
@@ -134,26 +137,32 @@ class OpenaiService implements IOpenaiService {
     return newVectorStoreDocument;
   }
 
-  private async uploadFile(pageId: Types.ObjectId, body: string): Promise<OpenAI.Files.FileObject> {
-    const file = await toFile(Readable.from(body), `${pageId}.md`);
-    const uploadedFile = await this.client.uploadFile(file);
-    return uploadedFile;
+  private async uploadFileByChunks(pageId: Types.ObjectId, body: string, vectorStoreFileRelationsMap: VectorStoreFileRelationsMap) {
+    const chunks = await splitMarkdownIntoChunks(body, 'gpt-4o');
+    for await (const [index, chunk] of chunks.entries()) {
+      try {
+        const file = await toFile(Readable.from(chunk), `${pageId}-chunk-${index}.md`);
+        const uploadedFile = await this.client.uploadFile(file);
+        prepareVectorStoreFileRelations(pageId, uploadedFile.id, vectorStoreFileRelationsMap);
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    }
   }
 
   async createVectorStoreFile(pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
-    const vectorStoreFileRelationsMap: Map<string, VectorStoreFileRelation> = new Map();
+    const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
     const processUploadFile = async(page: PageDocument) => {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
-          const uploadedFile = await this.uploadFile(page._id, page.revision.body);
-          prepareVectorStoreFileRelations(page._id, uploadedFile.id, vectorStoreFileRelationsMap);
+          await this.uploadFileByChunks(page._id, page.revision.body, vectorStoreFileRelationsMap);
           return;
         }
 
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
-          const uploadedFile = await this.uploadFile(page._id, pagePopulatedToShowRevision.revision.body);
-          prepareVectorStoreFileRelations(page._id, uploadedFile.id, vectorStoreFileRelationsMap);
+          await this.uploadFileByChunks(page._id, pagePopulatedToShowRevision.revision.body, vectorStoreFileRelationsMap);
         }
       }
     };
@@ -177,6 +186,8 @@ class OpenaiService implements IOpenaiService {
       return;
     }
 
+    const pageIds = pages.map(page => page._id);
+
     try {
       // Save vector store file relation
       await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
@@ -185,12 +196,14 @@ class OpenaiService implements IOpenaiService {
       const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
       const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStore.vectorStoreId, uploadedFileIds);
       logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
+
+      // Set isAttachedToVectorStore: true when the uploaded file is attached to VectorStore
+      await VectorStoreFileRelationModel.markAsAttachedToVectorStore(pageIds);
     }
     catch (err) {
       logger.error(err);
 
       // Delete all uploaded files if createVectorStoreFileBatch fails
-      const pageIds = pages.map(page => page._id);
       for await (const pageId of pageIds) {
         await this.deleteVectorStoreFile(pageId);
       }

+ 8 - 7
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -1,15 +1,17 @@
 import type { IUserHasId } from '@growi/core';
-import { Router, Request } from 'express';
+import type { Request } from 'express';
+import { Router } from 'express';
 import { body, validationResult } from 'express-validator';
 
-import Crowi from '~/server/crowi';
-import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 
-import { IAnswer } from '../../../interfaces/answer';
-import { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
-import { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
+import type { IAnswer } from '../../../interfaces/answer';
+import type { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
+import type { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
 import { StatusType } from '../../../interfaces/questionnaire-answer-status';
 import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
 import QuestionnaireAnswer from '../../models/questionnaire-answer';
@@ -25,7 +27,6 @@ interface AuthorizedRequest extends Request {
 }
 
 module.exports = (crowi: Crowi): Router => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequired = require('~/server/middlewares/login-required')(crowi, true);
 
   const validators = {

+ 1 - 1
apps/app/src/migrations/20180926134048-make-email-unique.js

@@ -11,7 +11,7 @@ module.exports = {
 
   async up(db, next) {
     logger.info('Start migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
 

+ 2 - 3
apps/app/src/migrations/20180927102719-init-serverurl.js

@@ -1,6 +1,5 @@
 import mongoose from 'mongoose';
 
-// eslint-disable-next-line import/no-named-as-default
 import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
@@ -21,7 +20,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
@@ -77,7 +76,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({

+ 2 - 2
apps/app/src/migrations/20181019114028-abolish-page-group-relation.js

@@ -31,7 +31,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     if (!isPagegrouprelationsExists) {
@@ -75,7 +75,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = pageModelFactory();
     const UserGroup = userGroupModelFactory();

+ 1 - 1
apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 module.exports = {
   async up(db, next) {
     logger.info('Start migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // enable passport and delete configs for crowi classic auth
     await Promise.all([

+ 2 - 2
apps/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -19,7 +19,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
 
@@ -49,7 +49,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({

+ 1 - 1
apps/app/src/migrations/20190619055421-adjust-page-grant.js

@@ -10,7 +10,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = getPageModel();
 

+ 1 - 1
apps/app/src/migrations/20190624110950-fill-last-update-user.js

@@ -13,7 +13,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = getPageModel();
 

+ 1 - 1
apps/app/src/migrations/20190629193445-make-root-page-public.js

@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:make-root-page-public');
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = getPageModel();
 

+ 1 - 1
apps/app/src/migrations/20191102223900-drop-configs-indices.js

@@ -14,7 +14,7 @@ async function dropIndexIfExists(collection, indexName) {
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const collection = db.collection('configs');
     await dropIndexIfExists(collection, 'ns_1');

+ 1 - 1
apps/app/src/migrations/20191102223901-drop-pages-indices.js

@@ -21,7 +21,7 @@ async function dropIndexIfExists(db, collectionName, indexName) {
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await dropIndexIfExists(db, 'pages', 'lastUpdateUser_1');
     await dropIndexIfExists(db, 'pages', 'liker_1');

+ 1 - 1
apps/app/src/migrations/20191126173016-adjust-pages-path.js

@@ -11,7 +11,7 @@ const logger = loggerFactory('growi:migrate:adjust-pages-path');
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = getPageModel();
 

+ 1 - 1
apps/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js

@@ -21,7 +21,7 @@ async function dropIndexIfExists(db, collectionName, indexName) {
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
 

+ 1 - 1
apps/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js

@@ -11,7 +11,7 @@ const logger = loggerFactory('growi:migrate:remove-deleteduser-from-relationgrou
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
 

+ 1 - 1
apps/app/src/migrations/20200420160390-remove-crowi-layout.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const query = { key: 'customize:layout', value: JSON.stringify('crowi') };
 

+ 2 - 2
apps/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:remove-behavior-type');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.findOneAndDelete({ key: 'customize:behavior' }); // remove behavior
 
@@ -20,7 +20,7 @@ module.exports = {
   async down(db, client) {
     // do not rollback
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const insertConfig = new Config({
       ns: 'crowi',

+ 1 - 1
apps/app/src/migrations/20200514001356-update-theme-color-for-dark.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Promise.all([
       await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('default-dark') }, { value: JSON.stringify('default') }), // update default-dark

+ 1 - 1
apps/app/src/migrations/20200620203632-normalize-locale-id.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:normalize-locale-id');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
 

+ 2 - 2
apps/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:remove-layout-setting');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const layoutType = await Config.findOne({ key: 'customize:layout' });
 
@@ -39,7 +39,7 @@ module.exports = {
 
   async down(db, client) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const theme = await Config.findOne({ key: 'customize:theme' });
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';

+ 2 - 2
apps/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:remove-layout-setting');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const [accessKeyId, secretAccessKey] = await Promise.all([
       Config.findOne({ key: 'aws:accessKeyId' }),
@@ -56,7 +56,7 @@ module.exports = {
 
   async down(db, client) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
 

+ 2 - 2
apps/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:update-mail-transmission');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const sesAccessKeyId = await Config.findOne({
       ns: 'crowi',
@@ -43,7 +43,7 @@ module.exports = {
 
   async down(db, client) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'mail:transmissionMethod'
     await Config.findOneAndDelete({

+ 2 - 2
apps/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -11,7 +11,7 @@ const mongoose = require('mongoose');
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.findOneAndDelete({ key: 'customize:isEnabledTimeline' }); // remove timeline
 
@@ -21,7 +21,7 @@ module.exports = {
   async down(db, client) {
     // do not rollback
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const insertConfig = new Config({
       ns: 'crowi',

+ 2 - 2
apps/app/src/migrations/20200915035234-rename-s3-config.js

@@ -34,7 +34,7 @@ const awsConfigs = [
 module.exports = {
   async up(db, client) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const request = awsConfigs.map((awsConfig) => {
       return {
@@ -53,7 +53,7 @@ module.exports = {
   async down(db, client) {
     logger.info('Rollback migration');
 
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const request = awsConfigs.map((awsConfig) => {
       return {

+ 1 - 1
apps/app/src/migrations/20210420160380-convert-double-to-date.js

@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = getModelSafely('Page') || getPageModel();
 

+ 2 - 2
apps/app/src/migrations/20210830074539-update-configs-for-slackbot.js

@@ -17,7 +17,7 @@ const keyMap = {
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     for await (const [oldKey, newKey] of Object.entries(keyMap)) {
       const isExist = (await Config.count({ key: newKey })) > 0;
@@ -37,7 +37,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     for await (const [oldKey, newKey] of Object.entries(keyMap)) {
       const isExist = (await Config.count({ key: oldKey })) > 0;

+ 1 - 1
apps/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:slack-app-integration-set-default-va
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const SlackAppIntegration = slackAppIntegrationFactory();
 

+ 2 - 2
apps/app/src/migrations/20210921173042-add-is-trashed-field.js

@@ -21,7 +21,7 @@ const updateIsPageTrashed = async(db, updateIdList) => {
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
 
     let updateDeletedPageIds = [];
@@ -52,7 +52,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     try {
       await db.collection('pagetagrelations').updateMany(

+ 2 - 2
apps/app/src/migrations/20211005120030-slack-app-integration-rename-keys.js

@@ -8,7 +8,7 @@ const logger = loggerFactory('growi:migrate:slack-app-integration-rename-keys');
 
 module.exports = {
   async up(db) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const SlackAppIntegration = slackAppIntegrationFactory();
 
@@ -45,7 +45,7 @@ module.exports = {
   },
 
   async down(db, next) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const SlackAppIntegration = slackAppIntegrationFactory();
 

+ 2 - 2
apps/app/src/migrations/20211005131430-config-without-proxy-command-permission-for-renaming.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:slack-app-integration-rename-keys');
 
 module.exports = {
   async up(db) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const isExist = (await Config.count({ key: 'slackbot:withoutProxy:commandPermission' })) > 0;
     if (!isExist) return;
@@ -53,7 +53,7 @@ module.exports = {
   },
 
   async down(db, next) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const isExist = (await Config.count({ key: 'slackbot:withoutProxy:commandPermission' })) > 0;
     if (!isExist) return next();

+ 2 - 2
apps/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:initialize-private-legacy-pages-name
 
 module.exports = {
   async up(db, next) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     try {
       await NamedQuery.updateOne(
@@ -29,7 +29,7 @@ module.exports = {
   },
 
   async down(db, next) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     try {
       await NamedQuery.findOneAndDelete({

+ 2 - 2
apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js

@@ -17,7 +17,7 @@ const LIMIT = 300;
 module.exports = {
   // path => pageId
   async up(db, client) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
 
     const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
@@ -67,7 +67,7 @@ module.exports = {
 
   // pageId => path
   async down(db, client) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
 
     const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });

+ 2 - 2
apps/app/src/migrations/20220131001218-convert-redirect-to-pages-to-page-redirect-documents.js

@@ -13,7 +13,7 @@ const BATCH_SIZE = 100;
 
 module.exports = {
   async up(db, client) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
     const pageCollection = await db.collection('pages');
     const PageRedirect = getModelSafely('PageRedirect') || PageRedirectModel;
 
@@ -49,7 +49,7 @@ module.exports = {
   },
 
   async down(db, client) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
     const pageCollection = await db.collection('pages');
     const PageRedirect = getModelSafely('PageRedirect') || PageRedirectModel;
 

+ 1 - 1
apps/app/src/migrations/20220311011114-convert-page-delete-config.js

@@ -12,7 +12,7 @@ const logger = loggerFactory('growi:migrate:convert-page-delete-config');
 
 module.exports = {
   async up(db, client) {
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const isNewConfigExists = await Config.count({
       ns: 'crowi',

+ 1 - 1
apps/app/src/migrations/20220411114257-set-sparse-option-to-slack-member-id.js

@@ -12,7 +12,7 @@ const logger = loggerFactory('growi:migrate:set-sparse-option-to-slack-member-id
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
     await User.syncIndexes();

+ 1 - 1
apps/app/src/migrations/20220613064207-add-attachment-type-to-existing-attachments.js

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:add-attachment-type-to-existing-atta
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // Add attachmentType for wiki page
     // Filter pages where "attachmentType" doesn't exist and "page" is not null

+ 2 - 2
apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js

@@ -11,7 +11,7 @@ const mongoose = require('mongoose');
 module.exports = {
   async up() {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.findOneAndDelete({ key: 'customize:isSavedStatesOfTabChanges' }); // remove isSavedStatesOfTabChanges
 
@@ -20,7 +20,7 @@ module.exports = {
 
   async down() {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const insertConfig = new Config({
       ns: 'crowi',

+ 1 - 1
apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -11,7 +11,7 @@ const mongoose = require('mongoose');
 module.exports = {
   async up() {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.findOneAndDelete({ key: 'security:passport-basic:isEnabled' });
     await Config.findOneAndDelete({ key: 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser' });

+ 2 - 2
apps/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -11,7 +11,7 @@ const mongoose = require('mongoose');
 module.exports = {
   async up() {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.findOneAndDelete({ key: 'markdown:presentation:pageBreakSeparator' });
     await Config.findOneAndDelete({ key: 'markdown:presentation:pageBreakCustomSeparator' });
@@ -21,7 +21,7 @@ module.exports = {
 
   async down() {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const insertConfig = new Config({
       ns: 'crowi',

+ 2 - 2
apps/app/src/migrations/20230731075753-add_installed_date_to_config.js

@@ -10,7 +10,7 @@ const mongoose = require('mongoose');
 module.exports = {
   async up() {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
     const User = userModelFactory();
 
     const appInstalled = await Config.findOne({ key: 'app:installed' });
@@ -36,7 +36,7 @@ module.exports = {
 
   async down() {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const appInstalled = await Config.findOne({ key: 'app:installed' });
     if (appInstalled != null) {

+ 1 - 1
apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js

@@ -11,7 +11,7 @@ const mongoose = require('mongoose');
 module.exports = {
   async up() {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     await UserUISettings.updateMany(
       {},

+ 9 - 0
apps/app/src/server/crowi/index.js

@@ -19,6 +19,7 @@ import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
 import UserEvent from '../events/user';
+import { accessTokenParser } from '../middlewares/access-token-parser';
 import { aclService as aclServiceSingletonInstance } from '../service/acl';
 import AppService from '../service/app';
 import AttachmentService from '../service/attachment';
@@ -51,6 +52,12 @@ const sep = path.sep;
 
 class Crowi {
 
+  /**
+   * For retrieving other packages
+   * @type {(req: import('express').Request, res: import('express').Response, next: import('express').NextFunction) => Promise<void>}
+   */
+  accessTokenParser;
+
   /** @type {AppService} */
   appService;
 
@@ -79,6 +86,8 @@ class Crowi {
 
     this.express = null;
 
+    this.accessTokenParser = accessTokenParser;
+
     this.config = {};
     this.configManager = null;
     this.s2sMessagingService = null;

+ 0 - 37
apps/app/src/server/middlewares/access-token-parser.js

@@ -1,37 +0,0 @@
-import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
-import mongoose from 'mongoose';
-
-import loggerFactory from '~/utils/logger';
-
-
-const logger = loggerFactory('growi:middleware:access-token-parser');
-
-module.exports = (crowi) => {
-
-  return async(req, res, next) => {
-    // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
-    const accessToken = req.query.access_token || req.body.access_token || null;
-    if (accessToken == null || typeof accessToken !== 'string') {
-      return next();
-    }
-
-    const User = mongoose.model('User');
-
-    logger.debug('accessToken is', accessToken);
-
-    const user = await User.findUserByApiToken(accessToken).lean();
-
-    if (user == null) {
-      logger.debug('The access token is invalid');
-      return next();
-    }
-
-    // transforming attributes
-    req.user = serializeUserSecurely(user);
-
-    logger.debug('Access token parsed.');
-
-    return next();
-  };
-
-};

+ 135 - 0
apps/app/src/server/middlewares/access-token-parser/access-token-parser.integ.ts

@@ -0,0 +1,135 @@
+import { faker } from '@faker-js/faker';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import type { Response } from 'express';
+import { mock } from 'vitest-mock-extended';
+
+import type Crowi from '~/server/crowi';
+import type UserEvent from '~/server/events/user';
+
+
+import type { AccessTokenParserReq } from './interfaces';
+
+import { accessTokenParser } from '.';
+
+
+vi.mock('@growi/core/dist/models/serializers', { spy: true });
+
+
+describe('access-token-parser middleware', () => {
+
+  let User;
+
+  beforeAll(async() => {
+    const crowiMock = mock<Crowi>({
+      event: vi.fn().mockImplementation((eventName) => {
+        if (eventName === 'user') {
+          return mock<UserEvent>({
+            on: vi.fn(),
+          });
+        }
+      }),
+    });
+    const userModelFactory = (await import('../../models/user')).default;
+    User = userModelFactory(crowiMock);
+  });
+
+  it('should call next if no access token is provided', async() => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+    const nextMock = vi.fn();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // act
+    await accessTokenParser(reqMock, resMock, nextMock);
+
+    // assert
+    expect(reqMock.user).toBeUndefined();
+    expect(serializeUserSecurely).not.toHaveBeenCalled();
+    expect(nextMock).toHaveBeenCalled();
+  });
+
+  it('should call next if the given access token is invalid', async() => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+    const nextMock = vi.fn();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // act
+    reqMock.query.access_token = 'invalidToken';
+    await accessTokenParser(reqMock, resMock, nextMock);
+
+    // assert
+    expect(reqMock.user).toBeUndefined();
+    expect(serializeUserSecurely).not.toHaveBeenCalled();
+    expect(nextMock).toHaveBeenCalled();
+  });
+
+  it('should set req.user with a valid access token in query', async() => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+    const nextMock = vi.fn();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // prepare a user with an access token
+    const targetUser = await User.create({
+      name: faker.person.fullName(),
+      username: faker.string.uuid(),
+      password: faker.internet.password(),
+      lang: 'en_US',
+      apiToken: faker.internet.password(),
+    });
+
+    // act
+    reqMock.query.access_token = targetUser.apiToken;
+    await accessTokenParser(reqMock, resMock, nextMock);
+
+    // assert
+    expect(reqMock.user).toBeDefined();
+    expect(reqMock.user?._id).toStrictEqual(targetUser._id);
+    expect(serializeUserSecurely).toHaveBeenCalledOnce();
+    expect(nextMock).toHaveBeenCalled();
+  });
+
+  it('should set req.user with a valid access token in body', async() => {
+    // arrange
+    const reqMock = mock<AccessTokenParserReq>({
+      user: undefined,
+    });
+    const resMock = mock<Response>();
+    const nextMock = vi.fn();
+
+    expect(reqMock.user).toBeUndefined();
+
+    // prepare a user with an access token
+    const targetUser = await User.create({
+      name: faker.person.fullName(),
+      username: faker.string.uuid(),
+      password: faker.internet.password(),
+      lang: 'en_US',
+      apiToken: faker.internet.password(),
+    });
+
+    // act
+    reqMock.body.access_token = targetUser.apiToken;
+    await accessTokenParser(reqMock, resMock, nextMock);
+
+    // assert
+    expect(reqMock.user).toBeDefined();
+    expect(reqMock.user?._id).toStrictEqual(targetUser._id);
+    expect(serializeUserSecurely).toHaveBeenCalledOnce();
+    expect(nextMock).toHaveBeenCalled();
+  });
+
+});

+ 38 - 0
apps/app/src/server/middlewares/access-token-parser/access-token-parser.ts

@@ -0,0 +1,38 @@
+import type { IUser, IUserHasId } from '@growi/core/dist/interfaces';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import type { NextFunction, Response } from 'express';
+import type { HydratedDocument } from 'mongoose';
+import mongoose from 'mongoose';
+
+import loggerFactory from '~/utils/logger';
+
+import type { AccessTokenParserReq } from './interfaces';
+
+const logger = loggerFactory('growi:middleware:access-token-parser');
+
+
+export const accessTokenParser = async(req: AccessTokenParserReq, res: Response, next: NextFunction): Promise<void> => {
+  // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
+  const accessToken = req.query.access_token ?? req.body.access_token;
+  if (accessToken == null || typeof accessToken !== 'string') {
+    return next();
+  }
+
+  const User = mongoose.model<HydratedDocument<IUser>, { findUserByApiToken }>('User');
+
+  logger.debug('accessToken is', accessToken);
+
+  const user: IUserHasId = await User.findUserByApiToken(accessToken);
+
+  if (user == null) {
+    logger.debug('The access token is invalid');
+    return next();
+  }
+
+  // transforming attributes
+  req.user = serializeUserSecurely(user);
+
+  logger.debug('Access token parsed.');
+
+  return next();
+};

+ 1 - 0
apps/app/src/server/middlewares/access-token-parser/index.ts

@@ -0,0 +1 @@
+export * from './access-token-parser';

+ 14 - 0
apps/app/src/server/middlewares/access-token-parser/interfaces.ts

@@ -0,0 +1,14 @@
+import type { IUserHasId } from '@growi/core/dist/interfaces';
+import type { IUserSerializedSecurely } from '@growi/core/dist/models/serializers';
+import type { Request } from 'express';
+
+type ReqQuery = {
+  access_token?: string,
+}
+type ReqBody = {
+  access_token?: string,
+}
+
+export interface AccessTokenParserReq extends Request<undefined, undefined, ReqBody, ReqQuery> {
+  user?: IUserSerializedSecurely<IUserHasId>,
+}

+ 5 - 11
apps/app/src/server/models/user.js

@@ -7,6 +7,8 @@ import { i18n } from '^/config/next-i18next.config';
 import { generateGravatarSrc } from '~/utils/gravatar';
 import loggerFactory from '~/utils/logger';
 
+import { aclService } from '../service/acl';
+import { configManager } from '../service/config-manager';
 import { getModelSafely } from '../util/mongoose-utils';
 
 import { Attachment } from './attachment';
@@ -99,8 +101,6 @@ const factory = (crowi) => {
   function decideUserStatusOnRegistration() {
     validateCrowi();
 
-    const { configManager, aclService } = crowi;
-
     const isInstalled = configManager.getConfig('crowi', 'app:installed');
     if (!isInstalled) {
       return STATUS_ACTIVE; // is this ok?
@@ -278,7 +278,7 @@ const factory = (crowi) => {
     this.name = name;
     this.username = username;
     this.status = STATUS_ACTIVE;
-    this.isEmailPublished = crowi.configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser');
+    this.isEmailPublished = configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser');
 
     this.save((err, userData) => {
       userEvent.emit('activated', userData);
@@ -371,7 +371,7 @@ const factory = (crowi) => {
   userSchema.statics.isEmailValid = function(email, callback) {
     validateCrowi();
 
-    const whitelist = crowi.configManager.getConfig('crowi', 'security:registrationWhitelist');
+    const whitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
 
     if (Array.isArray(whitelist) && whitelist.length > 0) {
       return whitelist.some((allowedEmail) => {
@@ -449,7 +449,7 @@ const factory = (crowi) => {
     if (apiToken == null) {
       return Promise.resolve(null);
     }
-    return this.findOne({ apiToken });
+    return this.findOne({ apiToken }).lean();
   };
 
   userSchema.statics.findUserByGoogleId = function(googleId, callback) {
@@ -480,8 +480,6 @@ const factory = (crowi) => {
   };
 
   userSchema.statics.isUserCountExceedsUpperLimit = async function() {
-    const { configManager } = crowi;
-
     const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
 
     const activeUsers = await this.countActiveUsers();
@@ -565,8 +563,6 @@ const factory = (crowi) => {
   };
 
   userSchema.statics.createUserByEmail = async function(email) {
-    const configManager = crowi.configManager;
-
     const User = this;
     const newUser = new User();
 
@@ -654,8 +650,6 @@ const factory = (crowi) => {
       newUser.setPassword(password);
     }
 
-    const configManager = crowi.configManager;
-
     // Default email show/hide is up to the administrator
     newUser.isEmailPublished = configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser');
 

+ 1 - 1
apps/app/src/server/routes/apiv3/activity.ts

@@ -5,6 +5,7 @@ import express from 'express';
 import { query } from 'express-validator';
 
 import type { IActivity, ISearchFilter } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
 
@@ -27,7 +28,6 @@ const validator = {
 
 module.exports = (crowi: Crowi): Router => {
   const adminRequired = require('../../middlewares/admin-required')(crowi);
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
   const router = express.Router();

+ 1 - 1
apps/app/src/server/routes/apiv3/app-settings.js

@@ -4,6 +4,7 @@ import { body } from 'express-validator';
 import { i18n } from '^/config/next-i18next.config';
 
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -143,7 +144,6 @@ const router = express.Router();
  */
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/attachment.js

@@ -6,6 +6,7 @@ import autoReap from 'multer-autoreap';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { Attachment } from '~/server/models/attachment';
 import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
 import loggerFactory from '~/utils/logger';
@@ -82,7 +83,6 @@ const {
  */
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const Page = crowi.model('Page');

+ 1 - 1
apps/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -3,6 +3,7 @@ import { body } from 'express-validator';
 import type { Types } from 'mongoose';
 
 import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
 import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
@@ -39,7 +40,6 @@ const validator = {
 };
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
   // Create new bookmark folder

+ 1 - 1
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -1,6 +1,7 @@
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
 import { preNotifyService } from '~/server/service/pre-notify';
@@ -68,7 +69,6 @@ const router = express.Router();
  */
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const addActivity = generateAddActivityMiddleware(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/export.js

@@ -1,4 +1,5 @@
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -38,7 +39,6 @@ const router = express.Router();
  */
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -7,6 +7,7 @@ import express from 'express';
 import { body } from 'express-validator';
 import multer from 'multer';
 
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
 import { configManager } from '~/server/service/config-manager';
 import type { IDataGROWIInfo } from '~/server/service/g2g-transfer';
@@ -83,7 +84,6 @@ module.exports = (crowi: Crowi): Router => {
 
   const isInstalled = crowi.configManager?.getConfig('crowi', 'app:installed');
 
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 

+ 1 - 1
apps/app/src/server/routes/apiv3/import.js

@@ -1,6 +1,7 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { getImportService } from '~/server/service/import';
 import { generateOverwriteParams } from '~/server/service/import/overwrite-params';
 import loggerFactory from '~/utils/logger';
@@ -58,7 +59,6 @@ export default function route(crowi) {
   const { growiBridgeService, socketIoService } = crowi;
   const importService = getImportService(crowi);
 
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -3,6 +3,7 @@ import express from 'express';
 
 import { SupportedAction } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 
 import type { IInAppNotification } from '../../../interfaces/in-app-notification';
@@ -14,7 +15,6 @@ const router = express.Router();
 
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 

+ 1 - 1
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -9,6 +9,7 @@ import { query, oneOf } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { configManager } from '~/server/service/config-manager';
 import type { IPageGrantService } from '~/server/service/page-grant';
 import loggerFactory from '~/utils/logger';
@@ -60,7 +61,6 @@ const validator = {
  * Routes
  */
 const routerFactory = (crowi: Crowi): Router => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
 
   const router = express.Router();

+ 2 - 2
apps/app/src/server/routes/apiv3/page/check-page-existence.ts

@@ -7,10 +7,11 @@ import { query } from 'express-validator';
 import mongoose from 'mongoose';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
-import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
 
@@ -30,7 +31,6 @@ type CreatePageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 export const checkPageExistenceHandlersFactory: CreatePageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
 
   // define validators for req.body

+ 1 - 1
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -17,6 +17,7 @@ import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 import type { IOptionsForCreate } from '~/interfaces/page';
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
@@ -101,7 +102,6 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
   const User = mongoose.model<IUser, { isExistUserByUserPagePath: any }>('User');
 
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
 

+ 1 - 1
apps/app/src/server/routes/apiv3/page/get-yjs-data.ts

@@ -6,6 +6,7 @@ import { param } from 'express-validator';
 import mongoose from 'mongoose';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
@@ -25,7 +26,6 @@ interface Req extends Request<ReqParams, ApiV3Response> {
 }
 export const getYjsDataHandlerFactory: GetYjsDataHandlerFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.params

+ 1 - 1
apps/app/src/server/routes/apiv3/page/index.ts

@@ -14,6 +14,7 @@ import sanitize from 'sanitize-filename';
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type { IPageGrantData } from '~/interfaces/page';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user';
@@ -107,7 +108,6 @@ const router = express.Router();
  *            example: 5e07345972560e001761fa63
  */
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const certifySharedPage = require('../../../middlewares/certify-shared-page')(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/page/publish-page.ts

@@ -6,6 +6,7 @@ import { param } from 'express-validator';
 import mongoose from 'mongoose';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
@@ -29,7 +30,6 @@ type PublishPageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 export const publishPageHandlersFactory: PublishPageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.body

+ 1 - 1
apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts

@@ -6,6 +6,7 @@ import { param, body } from 'express-validator';
 import mongoose from 'mongoose';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
 import { getYjsService } from '~/server/service/yjs';
 import loggerFactory from '~/utils/logger';
@@ -29,7 +30,6 @@ interface Req extends Request<ReqParams, ApiV3Response, ReqBody> {
 }
 export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.params

+ 1 - 1
apps/app/src/server/routes/apiv3/page/unpublish-page.ts

@@ -6,6 +6,7 @@ import { param } from 'express-validator';
 import mongoose from 'mongoose';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
@@ -29,7 +30,6 @@ type UnpublishPageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 export const unpublishPageHandlersFactory: UnpublishPageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.body

+ 1 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -16,6 +16,7 @@ import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
@@ -46,7 +47,6 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
   const Revision = mongoose.model<IRevisionHasId>('Revision');
 
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.body

+ 1 - 1
apps/app/src/server/routes/apiv3/pages/index.js

@@ -9,6 +9,7 @@ import { body, query } from 'express-validator';
 
 import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import PageTagRelation from '~/server/models/page-tag-relation';
 import { preNotifyService } from '~/server/service/pre-notify';
@@ -58,7 +59,6 @@ const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
  */
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const adminRequired = require('../../../middlewares/admin-required')(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -5,6 +5,7 @@ import { body } from 'express-validator';
 import { i18n } from '^/config/next-i18next.config';
 
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -66,7 +67,6 @@ const router = express.Router();
  *            type: string
  */
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 

+ 1 - 1
apps/app/src/server/routes/apiv3/revisions.js

@@ -3,6 +3,7 @@ import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import express from 'express';
 import { connection } from 'mongoose';
 
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { Revision } from '~/server/models/revision';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
 import loggerFactory from '~/utils/logger';
@@ -19,7 +20,6 @@ const MIGRATION_FILE_NAME = '20211227060705-revision-path-to-page-id-schema-migr
 
 module.exports = (crowi) => {
   const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
 
   const {

+ 1 - 1
apps/app/src/server/routes/apiv3/search.js

@@ -1,6 +1,7 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -17,7 +18,6 @@ const router = express.Router();
 const noCache = require('nocache');
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -9,6 +9,7 @@ import {
 } from '@growi/slack/dist/utils/check-communicable';
 
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -45,7 +46,6 @@ const router = express.Router();
 
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);

+ 1 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -10,6 +10,7 @@ import { isEmail } from 'validator';
 
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
 import { serializePageSecurely } from '~/server/models/serializers';
@@ -74,7 +75,6 @@ const validator = {};
  */
 
 module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);

+ 1 - 1
apps/app/src/server/routes/index.js

@@ -3,6 +3,7 @@ import express from 'express';
 
 import { middlewareFactory as rateLimiterFactory } from '~/features/rate-limiter';
 
+import { accessTokenParser } from '../middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
 import { excludeReadOnlyUser, excludeReadOnlyUserIfCommentNotAllowed } from '../middlewares/exclude-read-only-user';
@@ -30,7 +31,6 @@ module.exports = function(crowi, app) {
   const autoReconnectToSearch = require('../middlewares/auto-reconnect-to-search')(crowi);
   const applicationNotInstalled = require('../middlewares/application-not-installed')(crowi);
   const applicationInstalled = require('../middlewares/application-installed')(crowi);
-  const accessTokenParser = require('../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../middlewares/login-required')(crowi);
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const adminRequired = require('../middlewares/admin-required')(crowi);

+ 1 - 1
apps/app/test/integration/global-setup.js

@@ -8,7 +8,7 @@ if (process.env.NODE_ENV !== 'test') {
 }
 
 module.exports = async() => {
-  mongoose.connect(getMongoUri(), mongoOptions);
+  await mongoose.connect(getMongoUri(), mongoOptions);
 
   // drop database
   await mongoose.connection.dropDatabase();

+ 0 - 75
apps/app/test/integration/middlewares/access-token-parser.test.js

@@ -1,75 +0,0 @@
-const mongoose = require('mongoose');
-
-const { getInstance } = require('../setup-crowi');
-
-describe('accessTokenParser', () => {
-  let crowi;
-  let accessTokenParser;
-
-  let User;
-  let targetUser;
-
-  beforeAll(async() => {
-    crowi = await getInstance();
-    User = mongoose.model('User');
-    accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
-
-    targetUser = await User.create({
-      name: 'Example for access token parser',
-      username: 'targetUser',
-      password: 'usertestpass',
-      lang: 'en_US',
-      apiToken: 'N4xPDjh48TBsC7ahUN+ajjL5asnGpwtA5VAR+EhIDeg=',
-    });
-  });
-
-  crowi = {
-    model: jest.fn().mockReturnValue(User),
-  };
-  const req = {
-    query: {},
-    body: {},
-    user: {},
-  };
-
-  const res = {};
-  const next = jest.fn().mockReturnValue('next');
-
-  test('without accessToken', async() => {
-    const result = await accessTokenParser(req, res, next);
-
-    expect(next).toHaveBeenCalled();
-    expect(result).toBe('next');
-  });
-
-  test('with invalid accessToken', async() => {
-    req.query.access_token = 'invalidAccessToken';
-
-    const result = await accessTokenParser(req, res, next);
-
-    expect(next).toHaveBeenCalled();
-    expect(result).toBe('next');
-  });
-
-  test('with accessToken in query', async() => {
-    req.query.access_token = 'N4xPDjh48TBsC7ahUN+ajjL5asnGpwtA5VAR+EhIDeg=';
-
-    const result = await accessTokenParser(req, res, next);
-
-    expect(next).toHaveBeenCalled();
-    expect(result).toBe('next');
-    expect(req.user._id).toStrictEqual(targetUser._id);
-  });
-
-  test('with accessToken in body', async() => {
-    req.body.access_token = 'N4xPDjh48TBsC7ahUN+ajjL5asnGpwtA5VAR+EhIDeg=';
-
-    const result = await accessTokenParser(req, res, next);
-
-    expect(next).toHaveBeenCalled();
-    expect(result).toBe('next');
-    expect(req.user._id).toStrictEqual(targetUser._id);
-  });
-
-
-});

+ 0 - 2
packages/markdown-splitter/.eslintignore

@@ -1,2 +0,0 @@
-/dist/**
-/types/**

+ 0 - 5
packages/markdown-splitter/.eslintrc.cjs

@@ -1,5 +0,0 @@
-module.exports = {
-  extends: [
-    'weseek/react',
-  ],
-};

+ 0 - 1
packages/markdown-splitter/.gitignore

@@ -1 +0,0 @@
-/dist

+ 0 - 49
packages/markdown-splitter/package.json

@@ -1,49 +0,0 @@
-{
-  "name": "@growi/markdown-splitter",
-  "version": "1.0.0",
-  "license": "MIT",
-  "private": "true",
-  "type": "module",
-  "module": "dist/index.js",
-  "types": "dist/index.d.ts",
-  "files": [
-    "dist"
-  ],
-  "main": "dist/index.cjs",
-  "exports": {
-    ".": {
-      "import": "./dist/index.js",
-      "require": "./dist/index.cjs"
-    }
-  },
-  "scripts": {
-    "build": "vite build",
-    "clean": "shx rm -rf dist",
-    "dev": "vite build --mode dev",
-    "watch": "pnpm run dev -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,ts}",
-    "lint:typecheck": "tsc",
-    "lint": "npm-run-all -p lint:*",
-    "test": "vitest run --coverage"
-  },
-  "dependencies": {
-    "js-tiktoken": "^1.0.15",
-    "js-yaml": "^4.1.0",
-    "remark-frontmatter": "^5.0.0",
-    "remark-gfm": "^4.0.0",
-    "remark-parse": "^11.0.0",
-    "remark-stringify": "^11.0.0",
-    "unified": "^11.0.0"
-  },
-  "devDependencies": {
-    "@types/js-yaml": "^4.0.9",
-    "eslint-plugin-regex": "^1.8.0",
-    "hast-util-sanitize": "^4.1.0",
-    "pako": "^2.1.0",
-    "throttle-debounce": "^5.0.0"
-  },
-  "peerDependencies": {
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
-  }
-}

+ 0 - 2
packages/markdown-splitter/src/index.ts

@@ -1,2 +0,0 @@
-export * from './services/markdown-splitter';
-export * from './services/markdown-token-splitter';

+ 0 - 16
packages/markdown-splitter/tsconfig.json

@@ -1,16 +0,0 @@
-{
-  "$schema": "http://json.schemastore.org/tsconfig",
-  "extends": "../../tsconfig.base.json",
-  "compilerOptions": {
-    "baseUrl": ".",
-    "paths": {
-      "~/*": ["./src/*"]
-    },
-    "types": [
-      "vitest/globals"
-    ]
-  },
-  "include": [
-    "src", "test"
-  ]
-}

+ 0 - 39
packages/markdown-splitter/vite.config.ts

@@ -1,39 +0,0 @@
-import path from 'path';
-
-import glob from 'glob';
-import { nodeExternals } from 'rollup-plugin-node-externals';
-import { defineConfig } from 'vite';
-import dts from 'vite-plugin-dts';
-
-// https://vitejs.dev/config/
-export default defineConfig({
-  plugins: [
-    dts({
-      copyDtsFiles: true,
-    }),
-    {
-      ...nodeExternals({
-        devDeps: true,
-        builtinsPrefix: 'ignore',
-      }),
-      enforce: 'pre',
-    },
-  ],
-  build: {
-    outDir: 'dist',
-    sourcemap: true,
-    lib: {
-      entry: glob.sync(path.resolve(__dirname, 'src/**/*.ts'), {
-        ignore: '**/*.spec.ts',
-      }),
-      name: 'core-libs',
-      formats: ['es', 'cjs'],
-    },
-    rollupOptions: {
-      output: {
-        preserveModules: true,
-        preserveModulesRoot: 'src',
-      },
-    },
-  },
-});

+ 0 - 25
packages/markdown-splitter/vitest.config.ts

@@ -1,25 +0,0 @@
-import tsconfigPaths from 'vite-tsconfig-paths';
-import { defineConfig, coverageConfigDefaults } from 'vitest/config';
-
-export default defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
-  test: {
-    environment: 'node',
-    clearMocks: true,
-    globals: true,
-    coverage: {
-      exclude: [
-        ...coverageConfigDefaults.exclude,
-        'src/**/index.ts',
-      ],
-      thresholds: {
-        statements: 100,
-        branches: 100,
-        lines: 100,
-        functions: 100,
-      },
-    },
-  },
-});

+ 1 - 1
packages/remark-attachment-refs/src/server/routes/refs.ts

@@ -69,7 +69,7 @@ const loginRequiredFallback = (req, res) => {
 export const routesFactory = (crowi): any => {
 
   const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
-  const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
+  const accessTokenParser = crowi.accessTokenParser;
 
   const router = Router();
 

+ 9 - 3
packages/remark-lsx/src/server/index.ts

@@ -35,18 +35,24 @@ const lsxValidator = [
   query('options.*').optional().isString(),
 ];
 
-const paramValidator = (req: Request, _: Response, next: NextFunction) => {
+const paramValidator = (req: Request, res: Response, next: NextFunction) => {
   const errObjArray = validationResult(req);
+
   if (errObjArray.isEmpty()) {
     return next();
   }
-  return new Error('Invalid lsx parameter');
+
+  const errs = errObjArray.array().map((err) => {
+    return new Error(`Invalid lsx parameter: ${err.param}: ${err.msg}`);
+  });
+
+  res.status(400).json({ errors: errs.map(err => err.message) });
 };
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
 const middleware = (crowi: any, app: any): void => {
   const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
-  const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
+  const accessTokenParser = crowi.accessTokenParser;
 
   app.get('/_api/lsx', accessTokenParser, loginRequired, lsxValidator, paramValidator, listPages);
 };

+ 25 - 61
pnpm-lock.yaml

@@ -211,6 +211,9 @@ importers:
       '@browser-bunyan/console-formatted-stream':
         specifier: ^1.8.0
         version: 1.8.0
+      '@cspell/dynamic-import':
+        specifier: ^8.15.4
+        version: 8.15.4
       '@elastic/elasticsearch7':
         specifier: npm:@elastic/elasticsearch@^7.17.0
         version: '@elastic/elasticsearch@7.17.13'
@@ -394,6 +397,12 @@ importers:
       is-iso-date:
         specifier: ^0.0.1
         version: 0.0.1
+      js-tiktoken:
+        specifier: ^1.0.15
+        version: 1.0.15
+      js-yaml:
+        specifier: ^4.1.0
+        version: 4.1.0
       katex:
         specifier: ^0.16.11
         version: 0.16.11
@@ -613,6 +622,9 @@ importers:
       remark-rehype:
         specifier: ^11.1.1
         version: 11.1.1
+      remark-stringify:
+        specifier: ^11.0.0
+        version: 11.0.0
       remark-toc:
         specifier: ^9.0.0
         version: 9.0.0
@@ -698,9 +710,6 @@ importers:
       '@growi/editor':
         specifier: workspace:^
         version: link:../../packages/editor
-      '@growi/markdown-splitter':
-        specifier: workspace:^
-        version: link:../../packages/markdown-splitter
       '@growi/ui':
         specifier: workspace:^
         version: link:../../packages/ui
@@ -1189,52 +1198,6 @@ importers:
         specifier: ^13.6.19
         version: 13.6.19
 
-  packages/markdown-splitter:
-    dependencies:
-      js-tiktoken:
-        specifier: ^1.0.15
-        version: 1.0.15
-      js-yaml:
-        specifier: ^4.1.0
-        version: 4.1.0
-      react:
-        specifier: ^18.2.0
-        version: 18.2.0
-      react-dom:
-        specifier: ^18.2.0
-        version: 18.2.0(react@18.2.0)
-      remark-frontmatter:
-        specifier: ^5.0.0
-        version: 5.0.0
-      remark-gfm:
-        specifier: ^4.0.0
-        version: 4.0.0
-      remark-parse:
-        specifier: ^11.0.0
-        version: 11.0.0
-      remark-stringify:
-        specifier: ^11.0.0
-        version: 11.0.0
-      unified:
-        specifier: ^11.0.0
-        version: 11.0.5
-    devDependencies:
-      '@types/js-yaml':
-        specifier: ^4.0.9
-        version: 4.0.9
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
-      hast-util-sanitize:
-        specifier: ^4.1.0
-        version: 4.1.0
-      pako:
-        specifier: ^2.1.0
-        version: 2.1.0
-      throttle-debounce:
-        specifier: ^5.0.0
-        version: 5.0.2
-
   packages/pluginkit:
     dependencies:
       '@growi/core':
@@ -2470,6 +2433,10 @@ packages:
     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
     engines: {node: '>=0.1.90'}
 
+  '@cspell/dynamic-import@8.15.4':
+    resolution: {integrity: sha512-tr0F6EYN6qtniNvt1Uib+PgYQHeo4dQHXE2Optap+hYTOoQ2VoQ+SwBVjZ+Q2bmSAB0fmOyf0AvgsUtnWIpavw==}
+    engines: {node: '>=18.0'}
+
   '@cspotcode/source-map-support@0.8.1':
     resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
     engines: {node: '>=12'}
@@ -4044,9 +4011,6 @@ packages:
   '@types/jest@29.5.12':
     resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==}
 
-  '@types/js-yaml@4.0.9':
-    resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
-
   '@types/json-schema@7.0.11':
     resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
 
@@ -7256,9 +7220,6 @@ packages:
   hast-util-raw@9.0.4:
     resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==}
 
-  hast-util-sanitize@4.1.0:
-    resolution: {integrity: sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==}
-
   hast-util-sanitize@5.0.1:
     resolution: {integrity: sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==}
 
@@ -7474,6 +7435,9 @@ packages:
     engines: {node: '>=8'}
     hasBin: true
 
+  import-meta-resolve@4.1.0:
+    resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
+
   imurmurhash@0.1.4:
     resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
     engines: {node: '>=0.8.19'}
@@ -14022,6 +13986,10 @@ snapshots:
   '@colors/colors@1.5.0':
     optional: true
 
+  '@cspell/dynamic-import@8.15.4':
+    dependencies:
+      import-meta-resolve: 4.1.0
+
   '@cspotcode/source-map-support@0.8.1':
     dependencies:
       '@jridgewell/trace-mapping': 0.3.9
@@ -16135,8 +16103,6 @@ snapshots:
       expect: 29.7.0
       pretty-format: 29.7.0
 
-  '@types/js-yaml@4.0.9': {}
-
   '@types/json-schema@7.0.11': {}
 
   '@types/json-schema@7.0.6': {}
@@ -19899,10 +19865,6 @@ snapshots:
       web-namespaces: 2.0.1
       zwitch: 2.0.2
 
-  hast-util-sanitize@4.1.0:
-    dependencies:
-      '@types/hast': 2.3.4
-
   hast-util-sanitize@5.0.1:
     dependencies:
       '@types/hast': 3.0.4
@@ -20195,6 +20157,8 @@ snapshots:
       pkg-dir: 4.2.0
       resolve-cwd: 3.0.0
 
+  import-meta-resolve@4.1.0: {}
+
   imurmurhash@0.1.4: {}
 
   indent-string@2.1.0: