Jelajahi Sumber

Merge pull request #9917 from weseek/feat/156162-164871-slack-package-biome-migration

support: Migrate linter/formatter to biome for @growi/slack package
Yuki Takei 10 bulan lalu
induk
melakukan
48dfdbbd06
44 mengubah file dengan 511 tambahan dan 215 penghapusan
  1. 1 0
      .devcontainer/app/devcontainer.json
  2. 1 0
      .devcontainer/pdf-converter/devcontainer.json
  3. 1 0
      .vscode/settings.json
  4. 55 0
      biome.json
  5. 1 1
      package.json
  6. 1 1
      packages/slack/.eslintignore
  7. 0 5
      packages/slack/.eslintrc.cjs
  8. 1 1
      packages/slack/package.json
  9. 3 9
      packages/slack/src/consts/index.ts
  10. 3 3
      packages/slack/src/interfaces/channel.ts
  11. 3 3
      packages/slack/src/interfaces/connection-status.ts
  12. 2 2
      packages/slack/src/interfaces/growi-bot-event.ts
  13. 8 2
      packages/slack/src/interfaces/growi-command-processor.ts
  14. 4 4
      packages/slack/src/interfaces/growi-command.ts
  15. 9 6
      packages/slack/src/interfaces/growi-interaction-processor.ts
  16. 12 11
      packages/slack/src/interfaces/request-between-growi-and-proxy.ts
  17. 10 4
      packages/slack/src/interfaces/request-from-slack.ts
  18. 4 4
      packages/slack/src/interfaces/respond-util.ts
  19. 3 3
      packages/slack/src/interfaces/response-url.ts
  20. 1 1
      packages/slack/src/interfaces/slackbot-types.ts
  21. 12 6
      packages/slack/src/middlewares/parse-slack-interaction-request.ts
  22. 19 9
      packages/slack/src/middlewares/verify-growi-to-slack-request.ts
  23. 24 11
      packages/slack/src/middlewares/verify-slack-request.ts
  24. 46 13
      packages/slack/src/utils/block-kit-builder.ts
  25. 49 31
      packages/slack/src/utils/check-communicable.ts
  26. 4 1
      packages/slack/src/utils/generate-last-update-markdown.ts
  27. 12 4
      packages/slack/src/utils/get-supported-growi-actions-regexps.ts
  28. 14 10
      packages/slack/src/utils/interaction-payload-accessor.ts
  29. 3 4
      packages/slack/src/utils/logger/index.ts
  30. 3 1
      packages/slack/src/utils/payload-interaction-id-helpers.ts
  31. 4 3
      packages/slack/src/utils/permission-parser.ts
  32. 4 6
      packages/slack/src/utils/post-ephemeral-errors.ts
  33. 7 4
      packages/slack/src/utils/publish-initial-home-view.ts
  34. 0 2
      packages/slack/src/utils/reshape-contents-body.test.ts
  35. 8 4
      packages/slack/src/utils/reshape-contents-body.ts
  36. 51 27
      packages/slack/src/utils/respond-util-factory.ts
  37. 12 3
      packages/slack/src/utils/response-url.ts
  38. 0 1
      packages/slack/src/utils/slash-command-parser.test.ts
  39. 6 2
      packages/slack/src/utils/slash-command-parser.ts
  40. 15 3
      packages/slack/src/utils/webclient-factory.ts
  41. 2 6
      packages/slack/tsconfig.json
  42. 1 1
      packages/slack/vite.config.ts
  43. 1 3
      packages/slack/vitest.config.ts
  44. 91 0
      pnpm-lock.yaml

+ 1 - 0
.devcontainer/app/devcontainer.json

@@ -24,6 +24,7 @@
     "vscode": {
       "extensions": [
         "dbaeumer.vscode-eslint",
+        "biomejs.biome",
         "mhutchie.git-graph",
         "eamodio.gitlens",
         "github.vscode-pull-request-github",

+ 1 - 0
.devcontainer/pdf-converter/devcontainer.json

@@ -16,6 +16,7 @@
     "vscode": {
       "extensions": [
         "dbaeumer.vscode-eslint",
+        "biomejs.biome",
         "mhutchie.git-graph",
         "eamodio.gitlens"
       ],

+ 1 - 0
.vscode/settings.json

@@ -13,6 +13,7 @@
 
   "editor.codeActionsOnSave": {
     "source.fixAll.eslint": "explicit",
+    "source.fixAll.biome": "explicit",
     "source.fixAll.markdownlint": "explicit",
     "source.fixAll.stylelint": "explicit"
   },

+ 55 - 0
biome.json

@@ -0,0 +1,55 @@
+{
+  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
+  "files": {
+    "ignore": [
+      "dist/**",
+      "node_modules/**",
+      "coverage/**",
+      "vite.config.ts.timestamp-*",
+      ".pnpm-store/**",
+      ".turbo/**",
+      ".vscode/**",
+      "turbo.json",
+      "./bin/**",
+      "./tsconfig.base.json",
+      ".devcontainer/**",
+      ".eslintrc.js",
+      ".stylelintrc.json",
+      "package.json",
+
+      "./apps/**",
+      "./packages/core/**",
+      "./packages/core-styles/**",
+      "./packages/custom-icons/**",
+      "./packages/editor/**",
+      "./packages/pdf-converter-client/**",
+      "./packages/pluginkit/**",
+      "./packages/presentation/**",
+      "./packages/preset-templates/**",
+      "./packages/preset-themes/**",
+      "./packages/remark-attachment-refs/**",
+      "./packages/remark-drawio/**",
+      "./packages/remark-growi-directive/**",
+      "./packages/remark-lsx/**",
+      "./packages/ui/**"
+    ]
+  },
+  "formatter": {
+    "enabled": true,
+    "indentStyle": "space"
+  },
+  "organizeImports": {
+    "enabled": true
+  },
+  "linter": {
+    "enabled": true,
+    "rules": {
+      "recommended": true
+    }
+  },
+  "javascript": {
+    "formatter": {
+      "quoteStyle": "single"
+    }
+  }
+}

+ 1 - 1
package.json

@@ -38,11 +38,11 @@
     "version:preminor": "pnpm version preminor --preid=RC --no-git-tag-version",
     "version:premajor": "pnpm version premajor --preid=RC --no-git-tag-version"
   },
-  "dependencies": {},
   "// comments for defDependencies": {
     "vite-plugin-dts": "v4.2.1 causes the unexpected error 'Cannot find package 'vue-tsc''"
   },
   "devDependencies": {
+    "@biomejs/biome": "1.9.4",
     "@changesets/changelog-github": "^0.5.0",
     "@changesets/cli": "^2.27.3",
     "@faker-js/faker": "^9.0.1",

+ 1 - 1
packages/slack/.eslintignore

@@ -1 +1 @@
-/dist/**
+*

+ 0 - 5
packages/slack/.eslintrc.cjs

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

+ 1 - 1
packages/slack/package.json

@@ -43,7 +43,7 @@
     "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,ts}",
+    "lint:js": "biome check",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint": "npm-run-all -p lint:*",
     "test": "vitest run --coverage"

+ 3 - 9
packages/slack/src/consts/index.ts

@@ -2,9 +2,7 @@ export const REQUEST_TIMEOUT_FOR_GTOP = 10000;
 
 export const REQUEST_TIMEOUT_FOR_PTOG = 10000;
 
-export const supportedSlackCommands: string[] = [
-  '/growi',
-];
+export const supportedSlackCommands: string[] = ['/growi'];
 
 export const supportedGrowiCommands: string[] = [
   'search',
@@ -13,17 +11,13 @@ export const supportedGrowiCommands: string[] = [
   'help',
 ];
 
-export const defaultSupportedCommandsNameForBroadcastUse: string[] = [
-  'search',
-];
+export const defaultSupportedCommandsNameForBroadcastUse: string[] = ['search'];
 
 export const defaultSupportedCommandsNameForSingleUse: string[] = [
   'note',
   'keep',
 ];
 
-export const defaultSupportedSlackEventActions: string[] = [
-  'unfurl',
-];
+export const defaultSupportedSlackEventActions: string[] = ['unfurl'];
 
 export * from './required-scopes';

+ 3 - 3
packages/slack/src/interfaces/channel.ts

@@ -1,6 +1,6 @@
 export type IChannel = {
-  id: string,
-  name: string,
-}
+  id: string;
+  name: string;
+};
 
 export type IChannelOptionalId = Omit<IChannel, 'id'> & Partial<IChannel>;

+ 3 - 3
packages/slack/src/interfaces/connection-status.ts

@@ -1,4 +1,4 @@
 export type ConnectionStatus = {
-  error?: Error,
-  workspaceName?: string,
-}
+  error?: Error;
+  workspaceName?: string;
+};

+ 2 - 2
packages/slack/src/interfaces/growi-bot-event.ts

@@ -1,4 +1,4 @@
 export interface GrowiBotEvent<T> {
-  eventType: string,
-  event: T,
+  eventType: string;
+  event: T;
 }

+ 8 - 2
packages/slack/src/interfaces/growi-command-processor.ts

@@ -2,8 +2,14 @@ import type { AuthorizeResult } from '@slack/oauth';
 
 import type { GrowiCommand } from './growi-command';
 
-export interface GrowiCommandProcessor<ProcessCommandContext = {[key: string]: string}> {
+export interface GrowiCommandProcessor<
+  ProcessCommandContext = { [key: string]: string },
+> {
   shouldHandleCommand(growiCommand?: GrowiCommand): boolean;
 
-  processCommand(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, context?: ProcessCommandContext): Promise<void>
+  processCommand(
+    growiCommand: GrowiCommand,
+    authorizeResult: AuthorizeResult,
+    context?: ProcessCommandContext,
+  ): Promise<void>;
 }

+ 4 - 4
packages/slack/src/interfaces/growi-command.ts

@@ -1,6 +1,6 @@
 export type GrowiCommand = {
-  text: string,
-  responseUrl: string,
-  growiCommandType: string,
-  growiCommandArgs: string[],
+  text: string;
+  responseUrl: string;
+  growiCommandType: string;
+  growiCommandArgs: string[];
 };

+ 9 - 6
packages/slack/src/interfaces/growi-interaction-processor.ts

@@ -1,7 +1,6 @@
 import type { AuthorizeResult } from '@slack/oauth';
 
-import { InteractionPayloadAccessor } from '../utils/interaction-payload-accessor';
-
+import type { InteractionPayloadAccessor } from '../utils/interaction-payload-accessor';
 
 export interface InteractionHandledResult<V> {
   result?: V;
@@ -9,10 +8,14 @@ export interface InteractionHandledResult<V> {
 }
 
 export interface GrowiInteractionProcessor<V> {
-
-  shouldHandleInteraction(interactionPayloadAccessor: InteractionPayloadAccessor): boolean;
+  shouldHandleInteraction(
+    interactionPayloadAccessor: InteractionPayloadAccessor,
+  ): boolean;
 
   processInteraction(
-    authorizeResult: AuthorizeResult, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor): Promise<InteractionHandledResult<V>>;
-
+    authorizeResult: AuthorizeResult,
+    // biome-ignore lint/suspicious/noExplicitAny: ignore
+    interactionPayload: any,
+    interactionPayloadAccessor: InteractionPayloadAccessor,
+  ): Promise<InteractionHandledResult<V>>;
 }

+ 12 - 11
packages/slack/src/interfaces/request-between-growi-and-proxy.ts

@@ -3,23 +3,24 @@ import type { Request } from 'express';
 export interface BlockKitRequest {
   // Block Kit properties
   body: {
-    view?: string,
-    blocks?: string
-  },
+    view?: string;
+    blocks?: string;
+  };
 }
 
-export type RequestFromGrowi = Request & BlockKitRequest & {
-  // appended by GROWI
-  headers:{'x-growi-gtop-tokens'?:string},
+export type RequestFromGrowi = Request &
+  BlockKitRequest & {
+    // appended by GROWI
+    headers: { 'x-growi-gtop-tokens'?: string };
 
-  // will be extracted from header
-  tokenGtoPs: string[],
-};
+    // will be extracted from header
+    tokenGtoPs: string[];
+  };
 
 export type RequestFromProxy = Request & {
   // appended by Proxy
-  headers:{'x-growi-ptog-token'?:string},
+  headers: { 'x-growi-ptog-token'?: string };
 
   // will be extracted from header
-  tokenPtoG: string[],
+  tokenPtoG: string[];
 };

+ 10 - 4
packages/slack/src/interfaces/request-from-slack.ts

@@ -1,16 +1,22 @@
 import type { Request } from 'express';
 
 export interface IInteractionPayloadAccessor {
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   firstAction(): any;
 }
 
 export type RequestFromSlack = Request & {
   // appended by slack
-  headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number},
+  headers: {
+    'x-slack-signature'?: string;
+    'x-slack-request-timestamp': number;
+  };
 
   // appended by GROWI or slackbot-proxy
-  slackSigningSecret?:string,
+  slackSigningSecret?: string;
 
-  interactionPayload?: any,
-  interactionPayloadAccessor?: any,
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  interactionPayload?: any;
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  interactionPayloadAccessor?: any;
 };

+ 4 - 4
packages/slack/src/interfaces/respond-util.ts

@@ -1,8 +1,8 @@
 import type { RespondBodyForResponseUrl } from './response-url';
 
 export interface IRespondUtil {
-  respond(body: RespondBodyForResponseUrl): Promise<void>,
-  respondInChannel(body: RespondBodyForResponseUrl): Promise<void>,
-  replaceOriginal(body: RespondBodyForResponseUrl): Promise<void>,
-  deleteOriginal(): Promise<void>,
+  respond(body: RespondBodyForResponseUrl): Promise<void>;
+  respondInChannel(body: RespondBodyForResponseUrl): Promise<void>;
+  replaceOriginal(body: RespondBodyForResponseUrl): Promise<void>;
+  deleteOriginal(): Promise<void>;
 }

+ 3 - 3
packages/slack/src/interfaces/response-url.ts

@@ -1,6 +1,6 @@
-import type { KnownBlock, Block } from '@slack/web-api';
+import type { Block, KnownBlock } from '@slack/web-api';
 
 export type RespondBodyForResponseUrl = {
-  text?: string,
-  blocks?: (KnownBlock | Block)[],
+  text?: string;
+  blocks?: (KnownBlock | Block)[];
 };

+ 1 - 1
packages/slack/src/interfaces/slackbot-types.ts

@@ -4,4 +4,4 @@ export const SlackbotType = {
   CUSTOM_WITH_PROXY: 'customBotWithProxy',
 } as const;
 
-export type SlackbotType = typeof SlackbotType[keyof typeof SlackbotType]
+export type SlackbotType = (typeof SlackbotType)[keyof typeof SlackbotType];

+ 12 - 6
packages/slack/src/middlewares/parse-slack-interaction-request.ts

@@ -1,17 +1,23 @@
-import type { Response, NextFunction } from 'express';
+import type { NextFunction, Response } from 'express';
 
 import type { RequestFromSlack } from '../interfaces/request-from-slack';
 import { InteractionPayloadAccessor } from '../utils/interaction-payload-accessor';
 
-
-export const parseSlackInteractionRequest = (req: RequestFromSlack, res: Response, next: NextFunction): Record<string, any> | void => {
+export const parseSlackInteractionRequest = (
+  req: RequestFromSlack,
+  res: Response,
+  next: NextFunction,
+): void => {
   // There is no payload in the request from slack
   if (req.body.payload == null) {
-    return next();
+    next();
+    return;
   }
 
   req.interactionPayload = JSON.parse(req.body.payload);
-  req.interactionPayloadAccessor = new InteractionPayloadAccessor(req.interactionPayload);
+  req.interactionPayloadAccessor = new InteractionPayloadAccessor(
+    req.interactionPayload,
+  );
 
-  return next();
+  next();
 };

+ 19 - 9
packages/slack/src/middlewares/verify-growi-to-slack-request.ts

@@ -1,31 +1,41 @@
-import type { Response, NextFunction } from 'express';
+import type { NextFunction, Response } from 'express';
 import createError from 'http-errors';
 
 import type { RequestFromGrowi } from '../interfaces/request-between-growi-and-proxy';
 import loggerFactory from '../utils/logger';
 
-const logger = loggerFactory('@growi/slack:middlewares:verify-growi-to-slack-request');
+const logger = loggerFactory(
+  '@growi/slack:middlewares:verify-growi-to-slack-request',
+);
 
 /**
  * Verify if the request came from slack
  * See: https://api.slack.com/authentication/verifying-requests-from-slack
  */
-export const verifyGrowiToSlackRequest = (req: RequestFromGrowi, res: Response, next: NextFunction): Record<string, any> | void => {
+export const verifyGrowiToSlackRequest = (
+  req: RequestFromGrowi,
+  res: Response,
+  next: NextFunction,
+): void => {
   const str = req.headers['x-growi-gtop-tokens'];
 
   if (str == null) {
-    const message = 'The value of header \'x-growi-gtop-tokens\' must not be empty.';
+    const message =
+      "The value of header 'x-growi-gtop-tokens' must not be empty.";
     logger.warn(message, { body: req.body });
-    return next(createError(400, message));
+    next(createError(400, message));
+    return;
   }
 
-  const tokens = str.split(',').map(value => value.trim());
+  const tokens = str.split(',').map((value) => value.trim());
   if (tokens.length === 0) {
-    const message = 'The value of header \'x-growi-gtop-tokens\' must include at least one or more tokens.';
+    const message =
+      "The value of header 'x-growi-gtop-tokens' must include at least one or more tokens.";
     logger.warn(message, { body: req.body });
-    return next(createError(400, message));
+    next(createError(400, message));
+    return;
   }
 
   req.tokenGtoPs = tokens;
-  return next();
+  next();
 };

+ 24 - 11
packages/slack/src/middlewares/verify-slack-request.ts

@@ -1,6 +1,6 @@
-import { createHmac, timingSafeEqual } from 'crypto';
+import { createHmac, timingSafeEqual } from 'node:crypto';
 
-import type { Response, NextFunction } from 'express';
+import type { NextFunction, Response } from 'express';
 import createError from 'http-errors';
 import { stringify } from 'qs';
 
@@ -13,13 +13,19 @@ const logger = loggerFactory('@growi/slack:middlewares:verify-slack-request');
  * Verify if the request came from slack
  * See: https://api.slack.com/authentication/verifying-requests-from-slack
  */
-export const verifySlackRequest = (req: RequestFromSlack & { rawBody: any }, res: Response, next: NextFunction): Record<string, any> | void => {
+export const verifySlackRequest = (
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  req: RequestFromSlack & { rawBody: any },
+  res: Response,
+  next: NextFunction,
+): void => {
   const signingSecret = req.slackSigningSecret;
 
   if (signingSecret == null) {
     const message = 'No signing secret.';
     logger.warn(message, { body: req.body });
-    return next(createError(400, message));
+    next(createError(400, message));
+    return;
   }
 
   // take out slackSignature and timestamp from header
@@ -29,7 +35,8 @@ export const verifySlackRequest = (req: RequestFromSlack & { rawBody: any }, res
   if (slackSignature == null || timestamp == null) {
     const message = 'Forbidden. Enter from Slack workspace';
     logger.warn(message, { body: req.body });
-    return next(createError(403, message));
+    next(createError(403, message));
+    return;
   }
 
   // protect against replay attacks
@@ -37,7 +44,8 @@ export const verifySlackRequest = (req: RequestFromSlack & { rawBody: any }, res
   if (Math.abs(time - timestamp) > 300) {
     const message = 'Verification failed.';
     logger.warn(message, { body: req.body });
-    return next(createError(403, message));
+    next(createError(403, message));
+    return;
   }
 
   // use req.rawBody for Events API
@@ -45,8 +53,7 @@ export const verifySlackRequest = (req: RequestFromSlack & { rawBody: any }, res
   let sigBaseString: string;
   if (req.body.event != null) {
     sigBaseString = `v0:${timestamp}:${req.rawBody}`;
-  }
-  else {
+  } else {
     sigBaseString = `v0:${timestamp}:${stringify(req.body, { format: 'RFC1738' })}`;
   }
   // generate growi signature
@@ -56,11 +63,17 @@ export const verifySlackRequest = (req: RequestFromSlack & { rawBody: any }, res
   const growiSignature = `v0=${hashedSigningSecret}`;
 
   // compare growiSignature and slackSignature
-  if (timingSafeEqual(Buffer.from(growiSignature, 'utf8'), Buffer.from(slackSignature, 'utf8'))) {
-    return next();
+  if (
+    timingSafeEqual(
+      Buffer.from(growiSignature, 'utf8'),
+      Buffer.from(slackSignature, 'utf8'),
+    )
+  ) {
+    next();
+    return;
   }
 
   const message = 'Verification failed.';
   logger.warn(message, { body: req.body });
-  return next(createError(403, message));
+  next(createError(403, message));
 };

+ 46 - 13
packages/slack/src/utils/block-kit-builder.ts

@@ -1,10 +1,22 @@
 import type {
-  SectionBlock, HeaderBlock, InputBlock, DividerBlock, ActionsBlock,
-  Button, Overflow, Datepicker, Select, RadioButtons, Checkboxes, Action, MultiSelect, PlainTextInput, Option,
+  Action,
+  ActionsBlock,
   ActionsBlockElement,
+  Button,
+  Checkboxes,
+  Datepicker,
+  DividerBlock,
+  HeaderBlock,
+  InputBlock,
+  MultiSelect,
+  Option,
+  Overflow,
+  PlainTextInput,
+  RadioButtons,
+  SectionBlock,
+  Select,
 } from '@slack/types';
 
-
 export function divider(): DividerBlock {
   return {
     type: 'divider',
@@ -31,7 +43,13 @@ export function markdownSectionBlock(text: string): SectionBlock {
   };
 }
 
-export function inputSectionBlock(blockId: string, labelText: string, actionId: string, isMultiline: boolean, placeholder: string): InputBlock {
+export function inputSectionBlock(
+  blockId: string,
+  labelText: string,
+  actionId: string,
+  isMultiline: boolean,
+  placeholder: string,
+): InputBlock {
   return {
     type: 'input',
     block_id: blockId,
@@ -59,7 +77,15 @@ export function actionsBlock(...elements: ActionsBlockElement[]): ActionsBlock {
 }
 
 export function inputBlock(
-    element: Select | MultiSelect | Datepicker | PlainTextInput | RadioButtons | Checkboxes, blockId: string, labelText: string,
+  element:
+    | Select
+    | MultiSelect
+    | Datepicker
+    | PlainTextInput
+    | RadioButtons
+    | Checkboxes,
+  blockId: string,
+  labelText: string,
 ): InputBlock {
   return {
     type: 'input',
@@ -73,19 +99,22 @@ export function inputBlock(
 }
 
 type ButtonElement = {
-  text: string,
-  actionId: string,
-  style?: string,
-  value?:string
-}
+  text: string;
+  actionId: string;
+  style?: string;
+  value?: string;
+};
 
 /**
  * Button element
  * https://api.slack.com/reference/block-kit/block-elements#button
  */
 export function buttonElement({
-  text, actionId, style, value,
-}:ButtonElement): Button {
+  text,
+  actionId,
+  style,
+  value,
+}: ButtonElement): Button {
   const button: Button = {
     type: 'button',
     text: {
@@ -105,7 +134,11 @@ export function buttonElement({
  * Option object
  * https://api.slack.com/reference/block-kit/composition-objects#option
  */
-export function checkboxesElementOption(text: string, description: string, value: string): Option {
+export function checkboxesElementOption(
+  text: string,
+  description: string,
+  value: string,
+): Option {
   return {
     text: {
       type: 'mrkdwn',

+ 49 - 31
packages/slack/src/utils/check-communicable.ts

@@ -1,5 +1,4 @@
-
-import { WebClient } from '@slack/web-api';
+import type { WebClient } from '@slack/web-api';
 import axios, { type AxiosError } from 'axios';
 
 import { requiredScopes } from '../consts';
@@ -14,11 +13,12 @@ import { generateWebClient } from './webclient-factory';
  * @param serverUri Server URI to connect
  * @returns AxiosError when error is occured
  */
-export const connectToHttpServer = async(serverUri: string): Promise<void|AxiosError> => {
+export const connectToHttpServer = async (
+  serverUri: string,
+): Promise<undefined | AxiosError> => {
   try {
     await axios.get(serverUri, { maxRedirects: 0, timeout: 3000 });
-  }
-  catch (err) {
+  } catch (err) {
     return err as AxiosError;
   }
 };
@@ -28,7 +28,9 @@ export const connectToHttpServer = async(serverUri: string): Promise<void|AxiosE
  *
  * @returns AxiosError when error is occured
  */
-export const connectToSlackApiServer = async(): Promise<void|AxiosError> => {
+export const connectToSlackApiServer = async (): Promise<
+  undefined | AxiosError
+> => {
   return connectToHttpServer('https://slack.com/api/');
 };
 
@@ -36,7 +38,8 @@ export const connectToSlackApiServer = async(): Promise<void|AxiosError> => {
  * Test Slack API
  * @param client
  */
-const testSlackApiServer = async(client: WebClient): Promise<any> => {
+// biome-ignore lint/suspicious/noExplicitAny: ignore
+const testSlackApiServer = async (client: WebClient): Promise<any> => {
   const result = await client.api.test();
 
   if (!result.ok) {
@@ -46,12 +49,17 @@ const testSlackApiServer = async(client: WebClient): Promise<any> => {
   return result;
 };
 
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 const checkSlackScopes = (resultTestSlackApiServer: any) => {
   const slackScopes = resultTestSlackApiServer.response_metadata.scopes;
-  const isPassedScopeCheck = requiredScopes.every(e => slackScopes.includes(e));
+  const isPassedScopeCheck = requiredScopes.every((e) =>
+    slackScopes.includes(e),
+  );
 
   if (!isPassedScopeCheck) {
-    throw new Error(`The scopes you registered are not appropriate. Required scopes are ${requiredScopes}`);
+    throw new Error(
+      `The scopes you registered are not appropriate. Required scopes are ${requiredScopes}`,
+    );
   }
 };
 
@@ -59,13 +67,14 @@ const checkSlackScopes = (resultTestSlackApiServer: any) => {
  * Retrieve Slack workspace name
  * @param client
  */
-const retrieveWorkspaceName = async(client: WebClient): Promise<string> => {
+const retrieveWorkspaceName = async (client: WebClient): Promise<string> => {
   const result = await client.team.info();
 
   if (!result.ok) {
     throw new Error(result.error);
   }
 
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   return (result as any).team?.name;
 };
 
@@ -73,7 +82,9 @@ const retrieveWorkspaceName = async(client: WebClient): Promise<string> => {
  * @param token bot OAuth token
  * @returns
  */
-export const getConnectionStatus = async(token:string): Promise<ConnectionStatus> => {
+export const getConnectionStatus = async (
+  token: string,
+): Promise<ConnectionStatus> => {
   const client = generateWebClient(token);
   const status: ConnectionStatus = {};
 
@@ -84,8 +95,7 @@ export const getConnectionStatus = async(token:string): Promise<ConnectionStatus
     await checkSlackScopes(resultTestSlackApiServer);
     // retrieve workspace name
     status.workspaceName = await retrieveWorkspaceName(client);
-  }
-  catch (err) {
+  } catch (err) {
     status.error = err as Error;
   }
 
@@ -98,35 +108,43 @@ export const getConnectionStatus = async(token:string): Promise<ConnectionStatus
  * @param botTokenResolver function to convert from key to token
  * @returns
  */
-export const getConnectionStatuses = async(keys: string[], botTokenResolver?: (key: string) => string): Promise<{[key: string]: ConnectionStatus}> => {
-  const map = keys
-    .reduce<Promise<Map<string, ConnectionStatus>>>(
-      async(acc, key) => {
-        let token = key;
-        if (botTokenResolver != null) {
-          token = botTokenResolver(key);
-        }
-        const status: ConnectionStatus = await getConnectionStatus(token);
-
-        (await acc).set(key, status);
-        return acc;
-      },
-      // define initial accumulator
-      Promise.resolve(new Map<string, ConnectionStatus>()),
-    );
+export const getConnectionStatuses = async (
+  keys: string[],
+  botTokenResolver?: (key: string) => string,
+): Promise<{ [key: string]: ConnectionStatus }> => {
+  const map = keys.reduce<Promise<Map<string, ConnectionStatus>>>(
+    async (acc, key) => {
+      let token = key;
+      if (botTokenResolver != null) {
+        token = botTokenResolver(key);
+      }
+      const status: ConnectionStatus = await getConnectionStatus(token);
+
+      (await acc).set(key, status);
+      return acc;
+    },
+    // define initial accumulator
+    Promise.resolve(new Map<string, ConnectionStatus>()),
+  );
 
   // convert to object
   return Object.fromEntries(await map);
 };
 
-export const sendSuccessMessage = async(token:string, channel:string, appSiteUrl:string): Promise<void> => {
+export const sendSuccessMessage = async (
+  token: string,
+  channel: string,
+  appSiteUrl: string,
+): Promise<void> => {
   const client = generateWebClient(token);
   await client.chat.postMessage({
     channel,
     text: 'Success',
     blocks: [
       markdownSectionBlock(`:tada: Successfully tested with ${appSiteUrl}.`),
-      markdownSectionBlock('Now your GROWI and Slack integration is ready to use :+1:'),
+      markdownSectionBlock(
+        'Now your GROWI and Slack integration is ready to use :+1:',
+      ),
     ],
   });
 };

+ 4 - 1
packages/slack/src/utils/generate-last-update-markdown.ts

@@ -1,6 +1,9 @@
 import { formatDistanceStrict } from 'date-fns/formatDistanceStrict';
 
-export function generateLastUpdateMrkdwn(updatedAt: string | Date | number, baseDate: Date): string {
+export function generateLastUpdateMrkdwn(
+  updatedAt: string | Date | number,
+  baseDate: Date,
+): string {
   if (updatedAt != null) {
     // cast to date
     const date = new Date(updatedAt);

+ 12 - 4
packages/slack/src/utils/get-supported-growi-actions-regexps.ts

@@ -1,7 +1,15 @@
-export const getSupportedGrowiActionsRegExps = (supportedGrowiCommands: string[]): RegExp[] => {
-  return supportedGrowiCommands.map(command => new RegExp(`^${command}:\\w+`));
+export const getSupportedGrowiActionsRegExps = (
+  supportedGrowiCommands: string[],
+): RegExp[] => {
+  return supportedGrowiCommands.map(
+    (command) => new RegExp(`^${command}:\\w+`),
+  );
 };
 
-export const getSupportedGrowiActionsRegExp = (supportedGrowiCommand: string): RegExp => {
-  return new RegExp(`(^${supportedGrowiCommand}$)|(^${supportedGrowiCommand}:\\w+)`);
+export const getSupportedGrowiActionsRegExp = (
+  supportedGrowiCommand: string,
+): RegExp => {
+  return new RegExp(
+    `(^${supportedGrowiCommand}$)|(^${supportedGrowiCommand}:\\w+)`,
+  );
 };

+ 14 - 10
packages/slack/src/utils/interaction-payload-accessor.ts

@@ -1,4 +1,4 @@
-import assert from 'assert';
+import assert from 'node:assert';
 
 import type { IChannel } from '../interfaces/channel';
 import type { IInteractionPayloadAccessor } from '../interfaces/request-from-slack';
@@ -7,16 +7,16 @@ import loggerFactory from './logger';
 
 const logger = loggerFactory('@growi/slack:utils:interaction-payload-accessor');
 
-
 export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
-
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   private payload: any;
 
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   constructor(payload: any) {
     this.payload = payload;
   }
 
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   firstAction(): any | null {
     const actions = this.payload.actions;
 
@@ -40,6 +40,7 @@ export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
     return responseUrls[0].response_url;
   }
 
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   getStateValues(): any | null {
     const state = this.payload.state;
     if (state != null && state.values != null) {
@@ -54,17 +55,18 @@ export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
     return null;
   }
 
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   getViewPrivateMetaData(): any | null {
     const view = this.payload.view;
 
-    if (view != null && view.private_metadata) {
+    if (view?.private_metadata) {
       return JSON.parse(view.private_metadata);
     }
 
     return null;
   }
 
-  getActionIdAndCallbackIdFromPayLoad(): {[key: string]: string} {
+  getActionIdAndCallbackIdFromPayLoad(): { [key: string]: string } {
     const actionId = this.firstAction()?.action_id || '';
     const callbackId = this.payload.view?.callback_id || '';
 
@@ -75,7 +77,9 @@ export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
     // private_metadata should have the channelName parameter when view_submission
     const privateMetadata = this.getViewPrivateMetaData();
     if (privateMetadata != null && privateMetadata.channelName != null) {
-      throw new Error('PrivateMetaDatas are not implemented after removal of modal from slash commands. Use payload instead.');
+      throw new Error(
+        'PrivateMetaDatas are not implemented after removal of modal from slash commands. Use payload instead.',
+      );
     }
     const channel = this.payload.channel;
     if (channel != null) {
@@ -85,6 +89,7 @@ export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
     return null;
   }
 
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   getOriginalData(): any | null {
     const value = this.firstAction()?.value;
     if (value == null) return null;
@@ -92,16 +97,15 @@ export class InteractionPayloadAccessor implements IInteractionPayloadAccessor {
     const { originalData } = JSON.parse(value);
     if (originalData == null) return JSON.parse(value);
 
+    // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
     let parsedOriginalData;
     try {
       parsedOriginalData = JSON.parse(originalData);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Failed to parse original data:\n', err);
       return null;
     }
 
     return parsedOriginalData;
   }
-
 }

+ 3 - 4
packages/slack/src/utils/logger/index.ts

@@ -1,11 +1,10 @@
-import Logger from 'bunyan';
+import type Logger from 'bunyan';
 import { createLogger } from 'universal-bunyan';
 
-const loggerFactory = function(name: string): Logger {
-  return createLogger({
+const loggerFactory = (name: string): Logger =>
+  createLogger({
     name,
     config: { default: 'info' },
   });
-};
 
 export default loggerFactory;

+ 3 - 1
packages/slack/src/utils/payload-interaction-id-helpers.ts

@@ -1,3 +1,5 @@
-export const getInteractionIdRegexpFromCommandName = (commandname: string): RegExp => {
+export const getInteractionIdRegexpFromCommandName = (
+  commandname: string,
+): RegExp => {
   return new RegExp(`^${commandname}:\\w+`);
 };

+ 4 - 3
packages/slack/src/utils/permission-parser.ts

@@ -1,8 +1,9 @@
 import type { IChannelOptionalId } from '../interfaces/channel';
 
-
-export const permissionParser = (permissionForCommand: boolean | string[], channel: IChannelOptionalId): boolean => {
-
+export const permissionParser = (
+  permissionForCommand: boolean | string[],
+  channel: IChannelOptionalId,
+): boolean => {
   if (permissionForCommand == null) {
     return false;
   }

+ 4 - 6
packages/slack/src/utils/post-ephemeral-errors.ts

@@ -3,12 +3,10 @@ import type { WebAPICallResult } from '@slack/web-api';
 import { markdownSectionBlock } from './block-kit-builder';
 import { respond } from './response-url';
 
-
-export const respondRejectedErrors = async(
-    rejectedResults: PromiseRejectedResult[],
-    responseUrl: string,
-): Promise<WebAPICallResult|void> => {
-
+export const respondRejectedErrors = async (
+  rejectedResults: PromiseRejectedResult[],
+  responseUrl: string,
+): Promise<WebAPICallResult | undefined> => {
   if (rejectedResults.length > 0) {
     await respond(responseUrl, {
       text: 'Error occured.',

+ 7 - 4
packages/slack/src/utils/publish-initial-home-view.ts

@@ -3,7 +3,10 @@
 
 import type { ViewsPublishResponse, WebClient } from '@slack/web-api';
 
-export const publishInitialHomeView = (client: WebClient, userId: string): Promise<ViewsPublishResponse> => {
+export const publishInitialHomeView = (
+  client: WebClient,
+  userId: string,
+): Promise<ViewsPublishResponse> => {
   return client.views.publish({
     user_id: userId,
     view: {
@@ -20,9 +23,9 @@ export const publishInitialHomeView = (client: WebClient, userId: string): Promi
           type: 'section',
           text: {
             type: 'mrkdwn',
-            text: 'Learn how to use GROWI Official bot.'
-            // eslint-disable-next-line max-len
-              + 'See <https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html#official-bot-settings | Docs>.',
+            text:
+              'Learn how to use GROWI Official bot.' +
+              'See <https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html#official-bot-settings | Docs>.',
           },
         },
       ],

+ 0 - 2
packages/slack/src/utils/reshape-contents-body.test.ts

@@ -1,7 +1,6 @@
 import { reshapeContentsBody } from './reshape-contents-body';
 
 describe('reshapeContentsBody', () => {
-
   describe('Markdown only', () => {
     test('Return the same input', () => {
       const input = `
@@ -110,5 +109,4 @@ some messages...\u0020\u0020
       expect(reshapeContentsBody(input)).toBe(output);
     });
   });
-
 });

+ 8 - 4
packages/slack/src/utils/reshape-contents-body.ts

@@ -40,7 +40,8 @@ const devideLinesBeforeAfterFirstHeader = (lines: string[]) => {
 // Reshape linesAfterFirstHeader
 export const reshapeContentsBody = (str: string): string => {
   const splitted = str.split('\n');
-  const { linesBeforeFirstHeader, linesAfterFirstHeader } = devideLinesBeforeAfterFirstHeader(splitted);
+  const { linesBeforeFirstHeader, linesAfterFirstHeader } =
+    devideLinesBeforeAfterFirstHeader(splitted);
   if (linesAfterFirstHeader.length === 0) {
     return linesBeforeFirstHeader.join('\n');
   }
@@ -64,7 +65,10 @@ export const reshapeContentsBody = (str: string): string => {
       }
       // ##*username*  HH:mm AM
       copyline = '\n## **'.concat(copyline);
-      copyline = copyline.replace(regexpTime, '**<span class="grw-keep-time">'.concat(time, '</span>\n'));
+      copyline = copyline.replace(
+        regexpTime,
+        '**<span class="grw-keep-time">'.concat(time, '</span>\n'),
+      );
     }
     // Check 3: Is this line a short time(HH:mm)?
     else if (regexpShortTime.test(copyline)) {
@@ -80,12 +84,12 @@ export const reshapeContentsBody = (str: string): string => {
     return copyline;
   });
   // remove all blanks
-  const blanksRemoved = reshapedArray.filter(line => line !== '');
+  const blanksRemoved = reshapedArray.filter((line) => line !== '');
   // add <div> to the first line & add </div> to the last line
   blanksRemoved[0] = '\n<div class="grw-keep">\n'.concat(blanksRemoved[0]);
   blanksRemoved.push('</div>');
   // Add 2 spaces and 1 enter to all lines
-  const completedArray = blanksRemoved.map(line => line.concat('  \n'));
+  const completedArray = blanksRemoved.map((line) => line.concat('  \n'));
   // join all
   const contentsBeforeFirstHeader = linesBeforeFirstHeader.join('');
   const contentsAfterFirstHeader = completedArray.join('');

+ 51 - 27
packages/slack/src/utils/respond-util-factory.ts

@@ -6,25 +6,30 @@ import type { RespondBodyForResponseUrl } from '../interfaces/response-url';
 
 type AxiosOptions = {
   headers?: {
-    [header:string]: string,
-  }
-}
+    [header: string]: string;
+  };
+};
 
 function getResponseUrlForProxy(proxyUri: string, responseUrl: string): string {
   return urljoin(proxyUri, `/g2s/respond?response_url=${responseUrl}`);
 }
 
 function getUrl(responseUrl: string, proxyUri: string | null): string {
-  return proxyUri == null ? responseUrl : getResponseUrlForProxy(proxyUri, responseUrl);
+  return proxyUri == null
+    ? responseUrl
+    : getResponseUrlForProxy(proxyUri, responseUrl);
 }
 
 export class RespondUtil implements IRespondUtil {
-
   url!: string;
 
   options!: AxiosOptions;
 
-  constructor(responseUrl: string, proxyUri: string | null, appSiteUrl: string) {
+  constructor(
+    responseUrl: string,
+    proxyUri: string | null,
+    appSiteUrl: string,
+  ) {
     this.url = getUrl(responseUrl, proxyUri);
 
     this.options = {
@@ -35,38 +40,57 @@ export class RespondUtil implements IRespondUtil {
   }
 
   async respond(body: RespondBodyForResponseUrl): Promise<void> {
-    return axios.post(this.url, {
-      replace_original: false,
-      text: body.text,
-      blocks: body.blocks,
-    }, this.options);
+    return axios.post(
+      this.url,
+      {
+        replace_original: false,
+        text: body.text,
+        blocks: body.blocks,
+      },
+      this.options,
+    );
   }
 
   async respondInChannel(body: RespondBodyForResponseUrl): Promise<void> {
-    return axios.post(this.url, {
-      response_type: 'in_channel',
-      replace_original: false,
-      text: body.text,
-      blocks: body.blocks,
-    }, this.options);
+    return axios.post(
+      this.url,
+      {
+        response_type: 'in_channel',
+        replace_original: false,
+        text: body.text,
+        blocks: body.blocks,
+      },
+      this.options,
+    );
   }
 
   async replaceOriginal(body: RespondBodyForResponseUrl): Promise<void> {
-    return axios.post(this.url, {
-      replace_original: true,
-      text: body.text,
-      blocks: body.blocks,
-    }, this.options);
+    return axios.post(
+      this.url,
+      {
+        replace_original: true,
+        text: body.text,
+        blocks: body.blocks,
+      },
+      this.options,
+    );
   }
 
   async deleteOriginal(): Promise<void> {
-    return axios.post(this.url, {
-      delete_original: true,
-    }, this.options);
+    return axios.post(
+      this.url,
+      {
+        delete_original: true,
+      },
+      this.options,
+    );
   }
-
 }
 
-export function generateRespondUtil(responseUrl: string, proxyUri: string | null, appSiteUrl: string): RespondUtil {
+export function generateRespondUtil(
+  responseUrl: string,
+  proxyUri: string | null,
+  appSiteUrl: string,
+): RespondUtil {
   return new RespondUtil(responseUrl, proxyUri, appSiteUrl);
 }

+ 12 - 3
packages/slack/src/utils/response-url.ts

@@ -2,7 +2,10 @@ import axios from 'axios';
 
 import type { RespondBodyForResponseUrl } from '../interfaces/response-url';
 
-export async function respond(responseUrl: string, body: RespondBodyForResponseUrl): Promise<void> {
+export async function respond(
+  responseUrl: string,
+  body: RespondBodyForResponseUrl,
+): Promise<void> {
   return axios.post(responseUrl, {
     replace_original: false,
     text: body.text,
@@ -10,7 +13,10 @@ export async function respond(responseUrl: string, body: RespondBodyForResponseU
   });
 }
 
-export async function respondInChannel(responseUrl: string, body: RespondBodyForResponseUrl): Promise<void> {
+export async function respondInChannel(
+  responseUrl: string,
+  body: RespondBodyForResponseUrl,
+): Promise<void> {
   return axios.post(responseUrl, {
     response_type: 'in_channel',
     replace_original: false,
@@ -19,7 +25,10 @@ export async function respondInChannel(responseUrl: string, body: RespondBodyFor
   });
 }
 
-export async function replaceOriginal(responseUrl: string, body: RespondBodyForResponseUrl): Promise<void> {
+export async function replaceOriginal(
+  responseUrl: string,
+  body: RespondBodyForResponseUrl,
+): Promise<void> {
   return axios.post(responseUrl, {
     replace_original: true,
     text: body.text,

+ 0 - 1
packages/slack/src/utils/slash-command-parser.test.ts

@@ -3,7 +3,6 @@ import { InvalidGrowiCommandError } from '../models/errors';
 import { parseSlashCommand } from './slash-command-parser';
 
 describe('parseSlashCommand', () => {
-
   describe('without growiCommandType', () => {
     test('throws InvalidGrowiCommandError', () => {
       // setup

+ 6 - 2
packages/slack/src/utils/slash-command-parser.ts

@@ -1,7 +1,9 @@
 import type { GrowiCommand } from '../interfaces/growi-command';
 import { InvalidGrowiCommandError } from '../models/errors';
 
-export const parseSlashCommand = (slashCommand:{[key:string]:string}): GrowiCommand => {
+export const parseSlashCommand = (slashCommand: {
+  [key: string]: string;
+}): GrowiCommand => {
   if (slashCommand.text == null) {
     throw new InvalidGrowiCommandError('The SlashCommand.text is null');
   }
@@ -10,7 +12,9 @@ export const parseSlashCommand = (slashCommand:{[key:string]:string}): GrowiComm
   const splitted = trimmedText.split(' ');
 
   if (splitted[0] === '') {
-    throw new InvalidGrowiCommandError('The SlashCommand.text does not specify GrowiCommand type');
+    throw new InvalidGrowiCommandError(
+      'The SlashCommand.text does not specify GrowiCommand type',
+    );
   }
 
   return {

+ 15 - 3
packages/slack/src/utils/webclient-factory.ts

@@ -9,18 +9,30 @@ const logLevel: LogLevel = isProduction ? LogLevel.DEBUG : LogLevel.INFO;
  * @param serverUri Slack Bot Token or Proxy Server URI
  * @param headers
  */
-export function generateWebClient(token?: string, serverUri?: string, headers?:{[key:string]:string}): WebClient;
+export function generateWebClient(
+  token?: string,
+  serverUri?: string,
+  headers?: { [key: string]: string },
+): WebClient;
 
 /**
  * Generate WebClilent instance
  * @param token
  * @param opts
  */
-export function generateWebClient(token?: string, opts?: WebClientOptions): WebClient;
+export function generateWebClient(
+  token?: string,
+  opts?: WebClientOptions,
+): WebClient;
 
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 export function generateWebClient(token?: string, ...args: any[]): WebClient {
   if (typeof args[0] === 'string') {
-    return new WebClient(token, { logLevel, slackApiUrl: args[0], headers: args[1] });
+    return new WebClient(token, {
+      logLevel,
+      slackApiUrl: args[0],
+      headers: args[1],
+    });
   }
 
   return new WebClient(token, { logLevel, ...args });

+ 2 - 6
packages/slack/tsconfig.json

@@ -6,11 +6,7 @@
     "paths": {
       "~/*": ["./src/*"]
     },
-    "types": [
-      "vitest/globals"
-    ]
+    "types": ["vitest/globals"]
   },
-  "include": [
-    "src"
-  ]
+  "include": ["src"]
 }

+ 1 - 1
packages/slack/vite.config.ts

@@ -1,4 +1,4 @@
-import path from 'path';
+import path from 'node:path';
 
 import glob from 'glob';
 import { nodeExternals } from 'rollup-plugin-node-externals';

+ 1 - 3
packages/slack/vitest.config.ts

@@ -2,9 +2,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
 import { defineConfig } from 'vitest/config';
 
 export default defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
+  plugins: [tsconfigPaths()],
   test: {
     environment: 'node',
     clearMocks: true,

+ 91 - 0
pnpm-lock.yaml

@@ -13,6 +13,9 @@ importers:
 
   .:
     devDependencies:
+      '@biomejs/biome':
+        specifier: 1.9.4
+        version: 1.9.4
       '@changesets/changelog-github':
         specifier: ^0.5.0
         version: 0.5.0(encoding@0.1.13)
@@ -2431,6 +2434,59 @@ packages:
   '@bcoe/v8-coverage@0.2.3':
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
 
+  '@biomejs/biome@1.9.4':
+    resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
+    engines: {node: '>=14.21.3'}
+    hasBin: true
+
+  '@biomejs/cli-darwin-arm64@1.9.4':
+    resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
+    engines: {node: '>=14.21.3'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@biomejs/cli-darwin-x64@1.9.4':
+    resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
+    engines: {node: '>=14.21.3'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@biomejs/cli-linux-arm64-musl@1.9.4':
+    resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
+    engines: {node: '>=14.21.3'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@biomejs/cli-linux-arm64@1.9.4':
+    resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
+    engines: {node: '>=14.21.3'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@biomejs/cli-linux-x64-musl@1.9.4':
+    resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
+    engines: {node: '>=14.21.3'}
+    cpu: [x64]
+    os: [linux]
+
+  '@biomejs/cli-linux-x64@1.9.4':
+    resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
+    engines: {node: '>=14.21.3'}
+    cpu: [x64]
+    os: [linux]
+
+  '@biomejs/cli-win32-arm64@1.9.4':
+    resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
+    engines: {node: '>=14.21.3'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@biomejs/cli-win32-x64@1.9.4':
+    resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
+    engines: {node: '>=14.21.3'}
+    cpu: [x64]
+    os: [win32]
+
   '@braintree/sanitize-url@7.1.0':
     resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==}
 
@@ -15988,6 +16044,41 @@ snapshots:
 
   '@bcoe/v8-coverage@0.2.3': {}
 
+  '@biomejs/biome@1.9.4':
+    optionalDependencies:
+      '@biomejs/cli-darwin-arm64': 1.9.4
+      '@biomejs/cli-darwin-x64': 1.9.4
+      '@biomejs/cli-linux-arm64': 1.9.4
+      '@biomejs/cli-linux-arm64-musl': 1.9.4
+      '@biomejs/cli-linux-x64': 1.9.4
+      '@biomejs/cli-linux-x64-musl': 1.9.4
+      '@biomejs/cli-win32-arm64': 1.9.4
+      '@biomejs/cli-win32-x64': 1.9.4
+
+  '@biomejs/cli-darwin-arm64@1.9.4':
+    optional: true
+
+  '@biomejs/cli-darwin-x64@1.9.4':
+    optional: true
+
+  '@biomejs/cli-linux-arm64-musl@1.9.4':
+    optional: true
+
+  '@biomejs/cli-linux-arm64@1.9.4':
+    optional: true
+
+  '@biomejs/cli-linux-x64-musl@1.9.4':
+    optional: true
+
+  '@biomejs/cli-linux-x64@1.9.4':
+    optional: true
+
+  '@biomejs/cli-win32-arm64@1.9.4':
+    optional: true
+
+  '@biomejs/cli-win32-x64@1.9.4':
+    optional: true
+
   '@braintree/sanitize-url@7.1.0': {}
 
   '@browser-bunyan/console-formatted-stream@1.8.0':