diff --git a/app/client/packages/rts/package.json b/app/client/packages/rts/package.json index 07d9ffec6985..d040bd90486b 100644 --- a/app/client/packages/rts/package.json +++ b/app/client/packages/rts/package.json @@ -30,8 +30,7 @@ "loglevel": "^1.8.1", "mongodb": "^5.8.0", "nodemailer": "6.9.9", - "readline-sync": "1.4.10", - "socket.io": "^4.6.2" + "readline-sync": "1.4.10" }, "devDependencies": { "@types/express": "^4.17.14", diff --git a/app/client/packages/rts/src/constants/routes.ts b/app/client/packages/rts/src/constants/routes.ts index d6f275c956c2..7996e1750d6e 100644 --- a/app/client/packages/rts/src/constants/routes.ts +++ b/app/client/packages/rts/src/constants/routes.ts @@ -1,6 +1,5 @@ const BASE_API_URL = "http://localhost:8091"; -export const RTS_BASE_PATH = "/rts"; export const RTS_BASE_API_PATH = "/rts-api/v1"; export const RTS_BASE_API_URL = `${BASE_API_URL}${RTS_BASE_API_PATH}`; diff --git a/app/client/packages/rts/src/constants/socket.ts b/app/client/packages/rts/src/constants/socket.ts deleted file mode 100644 index ae0e94195d68..000000000000 --- a/app/client/packages/rts/src/constants/socket.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const APP_ROOM_PREFIX = "app:"; -export const PAGE_ROOM_PREFIX = "page:"; -export const ROOT_NAMESPACE = "/"; -export const PAGE_EDIT_NAMESPACE = "/page/edit"; - -export const EDITORS_EVENT_NAME = "collab:online_editors"; -export const START_EDIT_EVENT_NAME = "collab:start_edit"; -export const LEAVE_EDIT_EVENT_NAME = "collab:leave_edit"; -export const MOUSE_POINTER_EVENT_NAME = "collab:mouse_pointer"; -export const RELEASE_VERSION_EVENT_NAME = "info:release_version"; -export const PAGE_VISIBILITY_EVENT_NAME = "info:page_visibility"; diff --git a/app/client/packages/rts/src/controllers/socket.ts b/app/client/packages/rts/src/controllers/socket.ts deleted file mode 100644 index 07c986ba4a9d..000000000000 --- a/app/client/packages/rts/src/controllers/socket.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { Server, Socket } from "socket.io"; -import { tryAuth } from "@middlewares/socket-auth"; -import { - START_EDIT_EVENT_NAME, - LEAVE_EDIT_EVENT_NAME, - MOUSE_POINTER_EVENT_NAME, - PAGE_EDIT_NAMESPACE, - PAGE_ROOM_PREFIX, - EDITORS_EVENT_NAME, -} from "@constants/socket"; -import type { Policy, MousePointerEvent } from "@utils/models"; -import { AppUser, CurrentEditorsEvent } from "@utils/models"; - -function subscribeToEditEvents(socket: Socket, appRoomPrefix: string) { - socket.on(START_EDIT_EVENT_NAME, (resourceId) => { - if (socket.data.email) { - // user is authenticated, join the room now - joinEditRoom(socket, resourceId, appRoomPrefix); - } else { - // user not authenticated yet, save the resource id and room prefix to join later after auth - socket.data.pendingRoomId = resourceId; - socket.data.pendingRoomPrefix = appRoomPrefix; - } - }); - - socket.on(LEAVE_EDIT_EVENT_NAME, (resourceId) => { - const roomName = appRoomPrefix + resourceId; - - socket.leave(roomName); // remove this socket from room - }); -} - -async function onAppSocketConnected(socket: Socket) { - const isAuthenticated = await tryAuthAndJoinPendingRoom(socket); - - if (isAuthenticated) { - socket.join("email:" + socket.data.email); - } -} - -async function onPageSocketConnected(socket: Socket, socketIo: Server) { - const isAuthenticated = await tryAuthAndJoinPendingRoom(socket); - - if (isAuthenticated) { - socket.on(MOUSE_POINTER_EVENT_NAME, (event: MousePointerEvent) => { - event.user = new AppUser(socket.data.name, socket.data.email); - event.socketId = socket.id; - socketIo - .of(PAGE_EDIT_NAMESPACE) - .to(PAGE_ROOM_PREFIX + event.pageId) - .emit(MOUSE_POINTER_EVENT_NAME, event); - }); - } -} - -async function tryAuthAndJoinPendingRoom(socket: Socket) { - const isAuthenticated = await tryAuth(socket); - - if (socket.data.pendingRoomId) { - // an appId or pageId is pending for this socket, join now - joinEditRoom( - socket, - socket.data.pendingRoomId, - socket.data.pendingRoomPrefix, - ); - } - - return isAuthenticated; -} - -function joinEditRoom(socket: Socket, roomId: string, roomPrefix: string) { - // remove this socket from any other rooms with roomPrefix - if (socket.rooms) { - socket.rooms.forEach((roomName) => { - if (roomName.startsWith(roomPrefix)) { - socket.leave(roomName); - } - }); - } - - // add this socket to room with application id - const roomName = roomPrefix + roomId; - - socket.join(roomName); -} - -function findPolicyEmails(policies: Policy[], permission: string): string[] { - const emails: string[] = []; - - for (const policy of policies) { - if (policy.permission === permission) { - for (const email of policy.users) { - emails.push(email); - } - - break; - } - } - - return emails; -} - -function sendCurrentUsers(socketIo, roomName: string, roomPrefix: string) { - if (roomName.startsWith(roomPrefix)) { - socketIo - .in(roomName) - .fetchSockets() - .then((sockets) => { - const onlineUsernames = new Set(); - const onlineUsers = new Array(); - - if (sockets) { - sockets.forEach((s) => { - if (!onlineUsernames.has(s.data.email)) { - onlineUsers.push(new AppUser(s.data.name, s.data.email)); - } - - onlineUsernames.add(s.data.email); - }); - } - - const resourceId = roomName.replace(roomPrefix, ""); // get resourceId from room name by removing the prefix - const response = new CurrentEditorsEvent(resourceId, onlineUsers); - - socketIo.to(roomName).emit(EDITORS_EVENT_NAME, response); - }); - } -} - -export { - subscribeToEditEvents, - onAppSocketConnected, - onPageSocketConnected, - sendCurrentUsers, - findPolicyEmails, -}; diff --git a/app/client/packages/rts/src/middlewares/socket-auth.ts b/app/client/packages/rts/src/middlewares/socket-auth.ts deleted file mode 100644 index 353a908f9c18..000000000000 --- a/app/client/packages/rts/src/middlewares/socket-auth.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { Socket } from "socket.io"; -import log from "loglevel"; -import axios from "axios"; - -import { BASE_APPSMITH_API_URL } from "@constants/routes"; - -export async function tryAuth(socket: Socket) { - /* ********************************************************* */ - // TODO: This change is not being used at the moment. Instead of using the environment variable API_BASE_URL - // we should be able to derive the API_BASE_URL from the host header. This will make configuration simpler - // for the user. The problem with this implementation is that Axios doesn't work for https endpoints currently. - // This needs to be debugged. - /* ********************************************************* */ - - // const host = socket.handshake.headers.host; - const connectionCookie = socket?.handshake?.headers?.cookie; - - if ( - connectionCookie === undefined || - connectionCookie === null || - connectionCookie === "" - ) { - return false; - } - - const matchedCookie = connectionCookie.match(/\bSESSION=\S+/); - - if (!matchedCookie) { - return false; - } - - const sessionCookie = matchedCookie[0]; - let response; - - try { - response = await axios.request({ - method: "GET", - url: BASE_APPSMITH_API_URL + "/users/me", - headers: { - Cookie: sessionCookie, - }, - }); - } catch (error) { - if (error.response?.status === 401) { - // eslint-disable-next-line no-console - console.info( - "401 received when authenticating user with cookie: " + sessionCookie, - ); - } else if (error.response) { - log.error( - "Error response received while authentication: ", - JSON.stringify(error.response.data), // this is so the message shows up in one line. - ); - } else { - log.error("Error authenticating", error.cause?.toString()); - } - - return false; - } - - const email = response?.data?.data?.email; - const name = response?.data?.data?.name ?? email; - - // If the session check API succeeds & the email/name is anonymousUser, then the user is not authenticated - // and we should not allow them to join any rooms - if (email == null || email === "anonymousUser" || name === "anonymousUser") { - return false; - } - - socket.data.email = email; - socket.data.name = name; - - return true; -} diff --git a/app/client/packages/rts/src/server.ts b/app/client/packages/rts/src/server.ts index 98c4fdac3418..665fc6ae69c2 100644 --- a/app/client/packages/rts/src/server.ts +++ b/app/client/packages/rts/src/server.ts @@ -1,18 +1,16 @@ import "./instrumentation"; import http from "http"; import express from "express"; -import { Server } from "socket.io"; import type { LogLevelDesc } from "loglevel"; import log from "loglevel"; import { VERSION as buildVersion } from "./version"; // release version of the api -import { initializeSockets } from "./sockets"; // routes import ast_routes from "./routes/ast_routes"; import dsl_routes from "./routes/dsl_routes"; import health_check_routes from "./routes/health_check_routes"; -import { RTS_BASE_PATH, RTS_BASE_API_PATH } from "@constants/routes"; +import { RTS_BASE_API_PATH } from "@constants/routes"; // Setting the logLevel for all log messages const logLevel: LogLevelDesc = (process.env.APPSMITH_LOG_LEVEL || @@ -27,11 +25,6 @@ const app = express(); app.disable("x-powered-by"); const server = new http.Server(app); -const io = new Server(server, { - path: RTS_BASE_PATH, -}); - -initializeSockets(io); // parse incoming json requests app.use(express.json({ limit: "5mb" })); diff --git a/app/client/packages/rts/src/sockets/events.ts b/app/client/packages/rts/src/sockets/events.ts deleted file mode 100644 index f1eb0a961cf0..000000000000 --- a/app/client/packages/rts/src/sockets/events.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { Server, Socket } from "socket.io"; -import log from "loglevel"; -import { - APP_ROOM_PREFIX, - RELEASE_VERSION_EVENT_NAME, - LEAVE_EDIT_EVENT_NAME, - PAGE_EDIT_NAMESPACE, - PAGE_ROOM_PREFIX, - ROOT_NAMESPACE, - PAGE_VISIBILITY_EVENT_NAME, -} from "@constants/socket"; -import { VERSION as buildVersion } from "../version"; -import { - subscribeToEditEvents, - onAppSocketConnected, - onPageSocketConnected, - sendCurrentUsers, -} from "@controllers/socket"; - -export function watchEvents(io: Server) { - io.on("connection", (socket: Socket) => { - socket.emit(RELEASE_VERSION_EVENT_NAME, buildVersion); - subscribeToEditEvents(socket, APP_ROOM_PREFIX); - onAppSocketConnected(socket).catch((error) => - log.error("Error in socket connected handler", error), - ); - }); - /** When we get the page visibility event, it means the page/tab - * is visible on the client after navigating away from it. - * We will respond back with the current version to - * so that the client can confirm if they are - * on the latest version of the client - */ - io.on(PAGE_VISIBILITY_EVENT_NAME, (socket: Socket) => { - socket.emit(RELEASE_VERSION_EVENT_NAME, buildVersion); - }); - - io.of(PAGE_EDIT_NAMESPACE).on("connection", (socket: Socket) => { - subscribeToEditEvents(socket, PAGE_ROOM_PREFIX); - onPageSocketConnected(socket, io).catch((error) => - log.error("Error in socket connected handler", error), - ); - }); - - io.of(ROOT_NAMESPACE).adapter.on("leave-room", (room, id) => { - if (room.startsWith(APP_ROOM_PREFIX)) { - log.debug(`ns:${ROOT_NAMESPACE}# socket ${id} left the room ${room}`); - } - - sendCurrentUsers(io, room, APP_ROOM_PREFIX); - }); - - io.of(ROOT_NAMESPACE).adapter.on("join-room", (room, id) => { - if (room.startsWith(APP_ROOM_PREFIX)) { - log.debug(`ns:${ROOT_NAMESPACE}# socket ${id} joined the room ${room}`); - } - - sendCurrentUsers(io, room, APP_ROOM_PREFIX); - }); - - io.of(PAGE_EDIT_NAMESPACE).adapter.on("leave-room", (room, id) => { - if (room.startsWith(PAGE_ROOM_PREFIX)) { - // someone left the page edit, notify others - log.debug( - `ns:${PAGE_EDIT_NAMESPACE} # socket ${id} left the room ${room}`, - ); - io.of(PAGE_EDIT_NAMESPACE).to(room).emit(LEAVE_EDIT_EVENT_NAME, id); - } - - sendCurrentUsers(io.of(PAGE_EDIT_NAMESPACE), room, PAGE_ROOM_PREFIX); - }); - - io.of(PAGE_EDIT_NAMESPACE).adapter.on("join-room", (room, id) => { - if (room.startsWith(PAGE_ROOM_PREFIX)) { - log.debug( - `ns:${PAGE_EDIT_NAMESPACE}# socket ${id} joined the room ${room}`, - ); - } - - sendCurrentUsers(io.of(PAGE_EDIT_NAMESPACE), room, PAGE_ROOM_PREFIX); - }); -} diff --git a/app/client/packages/rts/src/sockets/index.ts b/app/client/packages/rts/src/sockets/index.ts deleted file mode 100644 index 6c5c9d026025..000000000000 --- a/app/client/packages/rts/src/sockets/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { watchEvents } from "./events"; -import type { Server } from "socket.io"; - -// Initializing Multiple Sockets -export function initializeSockets(io: Server) { - watchEvents(io); -} diff --git a/app/client/packages/rts/src/utils/models.ts b/app/client/packages/rts/src/utils/models.ts deleted file mode 100644 index 1cfc04127720..000000000000 --- a/app/client/packages/rts/src/utils/models.ts +++ /dev/null @@ -1,32 +0,0 @@ -export class AppUser { - name: string; - email: string; - - constructor(name: string, email: string) { - this.name = name; - this.email = email; - } -} - -export class CurrentEditorsEvent { - resourceId: string; - users: AppUser[]; - - constructor(resourceId: string, users: AppUser[]) { - this.resourceId = resourceId; - this.users = users; - } -} - -export class MousePointerEvent { - pageId: string; - socketId: string; - user: AppUser; - data: object; -} - -export interface Policy { - permission: string; - users: string[]; - groups: string[]; -} diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 883016fcedae..b850a21ec50b 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -9040,13 +9040,6 @@ __metadata: languageName: node linkType: hard -"@socket.io/component-emitter@npm:~3.1.0": - version: 3.1.0 - resolution: "@socket.io/component-emitter@npm:3.1.0" - checksum: db069d95425b419de1514dffe945cc439795f6a8ef5b9465715acf5b8b50798e2c91b8719cbf5434b3fe7de179d6cdcd503c277b7871cb3dd03febb69bdd50fa - languageName: node - linkType: hard - "@storybook/addon-a11y@npm:^8.2.7": version: 8.2.7 resolution: "@storybook/addon-a11y@npm:8.2.7" @@ -10204,22 +10197,13 @@ __metadata: languageName: node linkType: hard -"@types/cookie@npm:^0.4.0, @types/cookie@npm:^0.4.1": +"@types/cookie@npm:^0.4.0": version: 0.4.1 resolution: "@types/cookie@npm:0.4.1" checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18 languageName: node linkType: hard -"@types/cors@npm:^2.8.12": - version: 2.8.13 - resolution: "@types/cors@npm:2.8.13" - dependencies: - "@types/node": "*" - checksum: 7ef197ea19d2e5bf1313b8416baa6f3fd6dd887fd70191da1f804f557395357dafd8bc8bed0ac60686923406489262a7c8a525b55748f7b2b8afa686700de907 - languageName: node - linkType: hard - "@types/cross-spawn@npm:^6.0.2": version: 6.0.2 resolution: "@types/cross-spawn@npm:6.0.2" @@ -10730,7 +10714,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0": +"@types/node@npm:*, @types/node@npm:>=13.7.0": version: 20.8.10 resolution: "@types/node@npm:20.8.10" dependencies: @@ -12693,7 +12677,6 @@ __metadata: mongodb: ^5.8.0 nodemailer: 6.9.9 readline-sync: 1.4.10 - socket.io: ^4.6.2 supertest: ^6.3.3 ts-jest: 29.1.0 typescript: ^5.5.4 @@ -13827,13 +13810,6 @@ __metadata: languageName: node linkType: hard -"base64id@npm:2.0.0, base64id@npm:~2.0.0": - version: 2.0.0 - resolution: "base64id@npm:2.0.0" - checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168 - languageName: node - linkType: hard - "batch@npm:0.6.1": version: 0.6.1 resolution: "batch@npm:0.6.1" @@ -15438,7 +15414,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.1, cookie@npm:~0.4.1": +"cookie@npm:^0.4.1": version: 0.4.2 resolution: "cookie@npm:0.4.2" checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b @@ -15498,16 +15474,6 @@ __metadata: languageName: node linkType: hard -"cors@npm:~2.8.5": - version: 2.8.5 - resolution: "cors@npm:2.8.5" - dependencies: - object-assign: ^4 - vary: ^1 - checksum: ced838404ccd184f61ab4fdc5847035b681c90db7ac17e428f3d81d69e2989d2b680cc254da0e2554f5ed4f8a341820a1ce3d1c16b499f6e2f47a1b9b07b5006 - languageName: node - linkType: hard - "cosmiconfig-typescript-loader@npm:^1.0.0": version: 1.0.9 resolution: "cosmiconfig-typescript-loader@npm:1.0.9" @@ -16384,7 +16350,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -17397,31 +17363,6 @@ __metadata: languageName: node linkType: hard -"engine.io-parser@npm:~5.2.1": - version: 5.2.2 - resolution: "engine.io-parser@npm:5.2.2" - checksum: 470231215f3136a9259efb1268bc9a71f789af4e8c74da8d3b49ceb149fe3cd5c315bf0cd13d2d8d9c8f0f051c6f93b68e8fa9c89a3b612b9217bf33765c943a - languageName: node - linkType: hard - -"engine.io@npm:~6.5.2": - version: 6.5.5 - resolution: "engine.io@npm:6.5.5" - dependencies: - "@types/cookie": ^0.4.1 - "@types/cors": ^2.8.12 - "@types/node": ">=10.0.0" - accepts: ~1.3.4 - base64id: 2.0.0 - cookie: ~0.4.1 - cors: ~2.8.5 - debug: ~4.3.1 - engine.io-parser: ~5.2.1 - ws: ~8.17.1 - checksum: 358d337dd007b81cd6d7f39d0161ec8ec3a86097f0fbb0e10240eace51f836741f93c3e6bd69322b9ce0ad0fd89253a41e09335b6eb412d13e5357a054a90c4a - languageName: node - linkType: hard - "enhanced-resolve@npm:^2.2.2": version: 2.3.0 resolution: "enhanced-resolve@npm:2.3.0" @@ -25715,7 +25656,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -31107,40 +31048,6 @@ __metadata: languageName: node linkType: hard -"socket.io-adapter@npm:~2.5.2": - version: 2.5.2 - resolution: "socket.io-adapter@npm:2.5.2" - dependencies: - ws: ~8.11.0 - checksum: 481251c3547221e57eb5cb247d0b1a3cde4d152a4c1c9051cc887345a7770e59f3b47f1011cac4499e833f01fcfc301ed13c4ec6e72f7dbb48a476375a6344cd - languageName: node - linkType: hard - -"socket.io-parser@npm:~4.2.4": - version: 4.2.4 - resolution: "socket.io-parser@npm:4.2.4" - dependencies: - "@socket.io/component-emitter": ~3.1.0 - debug: ~4.3.1 - checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263 - languageName: node - linkType: hard - -"socket.io@npm:^4.6.2": - version: 4.7.5 - resolution: "socket.io@npm:4.7.5" - dependencies: - accepts: ~1.3.4 - base64id: ~2.0.0 - cors: ~2.8.5 - debug: ~4.3.2 - engine.io: ~6.5.2 - socket.io-adapter: ~2.5.2 - socket.io-parser: ~4.2.4 - checksum: b8b57216152cf230bdcb77b5450e124ebe1fee7482eeb50a6ef760b69f2f5a064e9b8640ce9c1efc5c9e081f5d797d3f6ff3f81606e19ddaf5d4114aad9ec7d3 - languageName: node - linkType: hard - "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -33729,7 +33636,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1, vary@npm:~1.1.2": +"vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b @@ -34776,36 +34683,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:~8.11.0": - version: 8.11.0 - resolution: "ws@npm:8.11.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 - languageName: node - linkType: hard - -"ws@npm:~8.17.1": - version: 8.17.1 - resolution: "ws@npm:8.17.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf - languageName: node - linkType: hard - "xlsx@https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz": version: 0.19.3 resolution: "xlsx@https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz"