diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7c87c0fae..3f95db21c 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -45,6 +45,7 @@ "@types/node": "^22.13.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", + "happy-dom": "^20.11.2", "postcss": "^8.5.8", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", diff --git a/apps/dashboard/src/app/(dashboard)/settings/_components/GitHubConnection.tsx b/apps/dashboard/src/app/(dashboard)/settings/_components/GitHubConnection.tsx index 341e80a78..b541859c7 100644 --- a/apps/dashboard/src/app/(dashboard)/settings/_components/GitHubConnection.tsx +++ b/apps/dashboard/src/app/(dashboard)/settings/_components/GitHubConnection.tsx @@ -130,6 +130,14 @@ export function GitHubConnection() { return () => window.removeEventListener(GITHUB_SOURCES_CHANGED_EVENT, refreshForSourceChange); }, [loadStatus]); + // The provider finishes device/token sign-in asynchronously. This card owns + // a separate status snapshot, so refresh it when the pending action finishes. + const previousActionRef = useRef(cliAction); + useEffect(() => { + if (previousActionRef.current && !cliAction) void loadStatus(true); + previousActionRef.current = cliAction; + }, [cliAction, loadStatus]); + // Connect/install opens a separate window (OAuth popup or the GitHub App // install tab). The connect call returns as soon as that window opens, so // the immediate loadStatus below is stale. Arm this flag on click and @@ -285,12 +293,7 @@ export function GitHubConnection() { iconBg="bg-foreground/5" iconColor="text-foreground" > - {loading ? ( -
-
- {t.settings.github.checkingConnection} -
- ) : !anyConnected && cliAction ? ( + {cliAction ? ( /* A login is in flight. It's the only actionable thing on the card, so it replaces the chooser entirely instead of appearing underneath it. */ void loadStatus(true)} isDesktop={isDesktop} /> + ) : loading ? ( +
+
+ {t.settings.github.checkingConnection} +
) : anyConnected ? (
{/* The identity that is actually authorizing clones, first. */} diff --git a/apps/dashboard/src/app/(dashboard)/settings/_components/github-device-signin.test.tsx b/apps/dashboard/src/app/(dashboard)/settings/_components/github-device-signin.test.tsx new file mode 100644 index 000000000..35f824a4e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/settings/_components/github-device-signin.test.tsx @@ -0,0 +1,174 @@ +// @vitest-environment happy-dom + +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { GitHubProvider, type GitHubConnectionState } from "@/context/GitHubContext"; +import { GitHubConnection } from "./GitHubConnection"; + +const api = vi.hoisted(() => ({ + connect: vi.fn(), + pollConnect: vi.fn(), + getUserHome: vi.fn(), + getStatusDeduped: vi.fn(), + invalidateStatus: vi.fn(), + showToast: vi.fn(), +})); + +vi.mock("@/lib/api", () => ({ + githubApi: api, + settingsApi: { get: async () => ({}) }, + getApiErrorMessage: (error: Error) => error.message, + GITHUB_SOURCES_CHANGED_EVENT: "github-sources-changed", +})); +vi.mock("@/context/ToastContext", () => ({ useToast: () => ({ showToast: api.showToast }) })); +vi.mock("@/context/CloudContext", () => ({ useCloud: () => ({ connected: false }) })); +vi.mock("@/context/ModalContext", () => ({ useModal: () => ({}) })); +vi.mock("@/context/PlatformContext", () => ({ + usePlatform: () => ({ selfHosted: true, deployMode: "docker" }), +})); +vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) })); + +const disconnected: GitHubConnectionState = { + primary: null, + sources: { + openshipApp: { connected: false }, + ghCli: { available: false, problem: "rejected", method: "device" }, + }, +}; +const connected: GitHubConnectionState = { + primary: "gh-cli", + sources: { + openshipApp: { connected: false }, + ghCli: { available: true, login: "new-account", method: "device" }, + }, +}; +const deviceResponse = { + connected: false, + flow: "device_code", + userCode: "ABCD-1234", + verificationUri: "https://github.com/login/device", + expiresIn: 899, + interval: 5, +}; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + vi.useFakeTimers(); + vi.clearAllMocks(); + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + api.connect.mockResolvedValue(deviceResponse); + api.pollConnect.mockResolvedValue({ status: "pending" }); + api.getStatusDeduped.mockResolvedValue({ state: disconnected }); + api.getUserHome.mockResolvedValue({ state: disconnected }); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); +}); + +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); + vi.useRealTimers(); + vi.unstubAllGlobals(); +}); + +async function render(initialState = disconnected) { + await act(async () => { + root.render( + + + , + ); + }); +} + +async function click(label: string) { + const button = [...container.querySelectorAll("button")].find((el) => + el.textContent?.includes(label), + ); + expect(button, `button: ${label}`).toBeDefined(); + await act(async () => button!.click()); +} + +function expectDeviceInstructions() { + expect(container.textContent).toContain(deviceResponse.userCode); + expect(container.querySelector('a[href="https://github.com/login/device"]')).not.toBeNull(); +} + +describe("Settings GitHub device sign-in (#851)", () => { + it("shows the returned code, copies it, and updates the card after polling completes", async () => { + const copy = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(); + await render(); + await click("Sign in with GitHub"); + expectDeviceInstructions(); + await click(deviceResponse.userCode); + expect(copy).toHaveBeenCalledWith(deviceResponse.userCode); + + api.pollConnect.mockResolvedValue({ status: "complete" }); + api.getStatusDeduped.mockResolvedValue({ state: connected }); + api.getUserHome.mockResolvedValue({ state: connected }); + await act(async () => vi.advanceTimersByTimeAsync(5000)); + + expect(container.textContent).not.toContain(deviceResponse.userCode); + expect(container.textContent).toContain("@new-account"); + expect(container.textContent).not.toContain("Sign in with GitHub"); + api.pollConnect.mockClear(); + await act(async () => vi.advanceTimersByTimeAsync(15000)); + expect(api.pollConnect).not.toHaveBeenCalled(); + copy.mockRestore(); + }); + + it("keeps device instructions when switching from an already connected App", async () => { + const appState: GitHubConnectionState = { + ...disconnected, + primary: "openship-app", + sources: { ...disconnected.sources, openshipApp: { connected: true, login: "app-user" } }, + }; + api.getStatusDeduped.mockResolvedValue({ state: appState }); + api.getUserHome.mockResolvedValue({ state: appState }); + await render(appState); + await click("Change method"); + await click("Sign in with GitHub"); + expectDeviceInstructions(); + await act(async () => vi.advanceTimersByTimeAsync(5000)); + expectDeviceInstructions(); + }); + + it("does not let stale provider connectivity dismiss a new device grant", async () => { + // The library provider can still have its initial verified identity while + // the Settings card's fresh probe reports that credential as rejected. + await render(connected); + await click("Sign in with GitHub"); + expectDeviceInstructions(); + await act(async () => vi.advanceTimersByTimeAsync(5000)); + expectDeviceInstructions(); + }); + + it("keeps the code visible while the card refreshes its status", async () => { + await render(); + let resolveStatus!: (value: { state: GitHubConnectionState }) => void; + api.getStatusDeduped.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveStatus = resolve; + }), + ); + await click("Sign in with GitHub"); + expectDeviceInstructions(); + await act(async () => resolveStatus({ state: disconnected })); + expectDeviceInstructions(); + }); + + it("returns to sign-in and surfaces the error when the device grant expires", async () => { + await render(); + await click("Sign in with GitHub"); + api.pollConnect.mockResolvedValue({ status: "error", message: "The device code expired" }); + await act(async () => vi.advanceTimersByTimeAsync(5000)); + expect(container.textContent).not.toContain(deviceResponse.userCode); + expect(container.textContent).toContain("Sign in with GitHub"); + expect(api.showToast).toHaveBeenCalledWith("The device code expired", "error", "GitHub"); + }); +}); diff --git a/apps/dashboard/src/context/GitHubContext.tsx b/apps/dashboard/src/context/GitHubContext.tsx index 88a77d713..3b4f608fb 100644 --- a/apps/dashboard/src/context/GitHubContext.tsx +++ b/apps/dashboard/src/context/GitHubContext.tsx @@ -257,7 +257,6 @@ export function GitHubProvider({ children, initialData }: GitHubProviderProps) { if (res?.capabilities) setCapabilities(res.capabilities as GitHubCapabilities); if (nextState.primary !== null) { - setCliAction(null); setAccounts(res.accounts ?? []); const primaryLogin = nextState.sources.openshipApp.login ?? nextState.sources.ghCli.login ?? ""; @@ -494,12 +493,12 @@ export function GitHubProvider({ children, initialData }: GitHubProviderProps) { }, [cliAction, refresh, showToast]); /* ── Auto-detect a completed login ──────────────────────────── */ - // Any pending CLI action (the device flow OR a `gh auth login` the operator ran - // on the instance) clears the moment the connection lands, so the UI never gets - // stuck showing a code/command after success. + // Only a terminal login completes through a status probe. A device grant has + // its own authoritative poll above; a previously connected App or stale CLI + // identity must not dismiss the new code before the operator authorizes it. useEffect(() => { - if (connected && cliAction) setCliAction(null); - }, [connected, cliAction]); + if (cliAction?.type === "terminal" && state.sources.ghCli.available) setCliAction(null); + }, [state.sources.ghCli.available, cliAction]); // Terminal (`gh auth login`) has no device code to poll — refresh the status // periodically so the UI flips to connected as soon as the operator finishes, diff --git a/bun.lock b/bun.lock index 1099b491b..68f604e1b 100644 --- a/bun.lock +++ b/bun.lock @@ -108,6 +108,7 @@ "@types/node": "^22.13.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", + "happy-dom": "^20.11.2", "postcss": "^8.5.8", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", @@ -1073,10 +1074,14 @@ "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], "@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -1243,6 +1248,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="], "bullmq": ["bullmq@5.70.4", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.9.3", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-S58YT/tGdhc4pEPcIahtZRBR1TcTLpss1UKiXimF+Vy4yZwF38pW2IvhHqs4j4dEbZqDt8oi0jGGN/WYQHbPDg=="], @@ -1501,7 +1508,7 @@ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -1741,6 +1748,8 @@ "gsap": ["gsap@3.14.2", "", {}, "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA=="], + "happy-dom": ["happy-dom@20.14.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-4bRh1KzRvKDnFNTlLhzT1RZTpkKhQbQDl9j+7GXszWsvuspYdo29k6OHRf4PwiM6oLb8r/pMWeYiJjkfod5AvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -2937,6 +2946,8 @@ "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -3119,6 +3130,8 @@ "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/ws/@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "api/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -3135,6 +3148,8 @@ "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-image-size/@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "bullmq/ioredis": ["ioredis@5.9.3", "", { "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA=="], "bullmq/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -3227,6 +3242,10 @@ "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "happy-dom/@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + + "happy-dom/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "load-json-file/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -3289,6 +3308,8 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-type/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -3429,6 +3450,8 @@ "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "api/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "api/ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -3445,6 +3468,8 @@ "appdmg/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + "buffer-image-size/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "bullmq/ioredis/@ioredis/commands": ["@ioredis/commands@1.5.0", "", {}, "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow=="], "cacache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -3539,6 +3564,8 @@ "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "happy-dom/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "log-update/ansi-escapes/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], "log-update/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],