diff --git a/.changeset/olive-jars-tap.md b/.changeset/olive-jars-tap.md new file mode 100644 index 0000000000..61108f9ba0 --- /dev/null +++ b/.changeset/olive-jars-tap.md @@ -0,0 +1,5 @@ +--- +'e2b': patch +--- + +Fix uploads of non-native `ReadableStream`s in the browser silently sending the text `[object ReadableStream]` instead of the data. Buffering a stream drained it with `new Response(stream)`, which accepts any async iterable on Node (an undici extension) but only its own stream class in a browser, stringifying anything else. Streams are now drained through the reader, which every implementation supports. diff --git a/.changeset/soft-moons-repeat.md b/.changeset/soft-moons-repeat.md new file mode 100644 index 0000000000..ab6668326e --- /dev/null +++ b/.changeset/soft-moons-repeat.md @@ -0,0 +1,5 @@ +--- +'e2b': patch +--- + +Report a sandbox killed mid-request as an actionable `TimeoutError` in the browser. When the connection to a sandbox drops mid-request the SDK probes the sandbox's health to tell a killed sandbox apart from a transient network blip, but the probe only ran for connection-dropped wordings it recognized, and the browser's (`network error`) was missing — so killing a sandbox while a command was running surfaced a generic `SandboxError: [unknown] network error` instead of `TimeoutError: ... The sandbox was killed or reached its end of life while the request was in flight.` diff --git a/.github/workflows/js_sdk_tests.yml b/.github/workflows/js_sdk_tests.yml index 3a08f0dcb8..c1a4396c9b 100644 --- a/.github/workflows/js_sdk_tests.yml +++ b/.github/workflows/js_sdk_tests.yml @@ -8,7 +8,7 @@ on: type: string default: '' node-only: - description: 'Run only the Node legs (skip Bun, Deno, and Cloudflare)' + description: 'Run only the Node legs (skip Bun, Deno, Cloudflare, and the browser)' required: false type: boolean default: false @@ -29,14 +29,17 @@ jobs: strategy: fail-fast: false # With node-only (set by staging callers) the matrix collapses to the - # Node legs: the Bun/Deno/Cloudflare legs re-run suites the Node legs - # already cover and add sandbox/build load without extra backend - # signal, so the other runtimes are exercised against production only. + # Node legs: the Bun/Deno/Cloudflare/browser legs re-run suites the Node + # legs already cover and add sandbox/build load, so the other runtimes + # are exercised against production only. Note this does drop backend + # signal for the browser leg specifically — it's the only one that + # depends on the API's CORS headers — so a staging-only CORS regression + # surfaces on the production run rather than before merge. matrix: include: >- ${{ inputs.node-only && fromJSON('[{"runtime": "node", "os": "ubuntu-22.04"}, {"runtime": "node", "os": "windows-latest"}]') - || fromJSON('[{"runtime": "node", "os": "ubuntu-22.04"}, {"runtime": "node", "os": "windows-latest"}, {"runtime": "bun", "os": "ubuntu-22.04"}, {"runtime": "deno", "os": "ubuntu-22.04"}, {"runtime": "cloudflare", "os": "ubuntu-22.04"}, {"runtime": "cloudflare-deploy", "os": "ubuntu-22.04"}]') }} + || fromJSON('[{"runtime": "node", "os": "ubuntu-22.04"}, {"runtime": "node", "os": "windows-latest"}, {"runtime": "bun", "os": "ubuntu-22.04"}, {"runtime": "deno", "os": "ubuntu-22.04"}, {"runtime": "cloudflare", "os": "ubuntu-22.04"}, {"runtime": "cloudflare-deploy", "os": "ubuntu-22.04"}, {"runtime": "browser", "os": "ubuntu-22.04"}]') }} runs-on: ${{ matrix.os }} # The cloudflare-deploy leg is advisory: it deploys to a brand-new # Cloudflare preview account on every run, so it inherits that account's @@ -82,22 +85,21 @@ jobs: run: | pnpm install --frozen-lockfile - # Only the Node runtime runs the vitest `browser` project, which drives - # Chromium through Playwright. + # Only the browser leg needs Chromium, which Playwright drives. - name: Get Playwright version - if: matrix.runtime == 'node' + if: matrix.runtime == 'browser' id: playwright-version run: echo "version=$(node -p "require('playwright/package.json').version")" >> "$GITHUB_OUTPUT" - name: Cache Playwright browsers - if: matrix.runtime == 'node' + if: matrix.runtime == 'browser' uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ matrix.os == 'windows-latest' && '~/AppData/Local/ms-playwright' || '~/.cache/ms-playwright' }} + path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - name: Install Playwright Chromium - if: matrix.runtime == 'node' + if: matrix.runtime == 'browser' run: pnpm run playwright:install # The unit bundle test and the Cloudflare deploy config fail in CI when @@ -153,3 +155,13 @@ jobs: env: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} + + # Full unit + connectionConfig suite inside Chromium (vitest browser mode). + # Chromium comes from the cached install step above; `pretest:browser` + # covers local runs, where it's a no-op once installed. + - name: Run test suite in the browser + if: matrix.runtime == 'browser' + run: pnpm test:browser + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index c39a846520..d17af19bb0 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -35,6 +35,8 @@ "generate:mcp": "json2ts -i ./../../spec/mcp-server.json -o src/sandbox/mcp.d.ts --unreachableDefinitions --style.singleQuote --no-style.semi", "check-deps": "knip", "playwright:install": "playwright install chromium", + "pretest:browser": "pnpm run playwright:install", + "test:browser": "vitest run --config tests/runtimes/browser/vitest.config.mts", "test:bun": "bunx --bun vitest run --project unit --project connectionConfig --project template", "test:cf": "vitest run --config tests/runtimes/cloudflare/vitest.config.mts", "test:cf:deploy": "vitest run --config tests/runtimes/cloudflare-deploy/vitest.config.mts", @@ -46,13 +48,9 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.18.7", "@redocly/cli": "2.46.1", - "@testing-library/react": "^16.3.2", "@types/node": "^20.19.19", "@types/platform": "^1.3.6", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", "@typescript/native": "npm:typescript@^7.0.2", - "@vitejs/plugin-react": "^4.3.4", "@vitest/browser": "^4.1.10", "@vitest/browser-playwright": "^4.1.10", "dotenv": "^16.4.5", @@ -63,12 +61,9 @@ "npm-run-all": "^4.1.5", "openapi-typescript": "^7.13.0", "playwright": "^1.62.1", - "react": "^19.2.8", - "react-dom": "^19.2.8", "tsdown": "^0.22.14", "typescript": "npm:@typescript/typescript6@^6.0.2", "vitest": "^4.1.10", - "vitest-browser-react": "^2.2.0", "wrangler": "^4.123.0" }, "files": [ diff --git a/packages/js-sdk/src/envd/rpc.ts b/packages/js-sdk/src/envd/rpc.ts index 9c5b4ad1ba..5e03225742 100644 --- a/packages/js-sdk/src/envd/rpc.ts +++ b/packages/js-sdk/src/envd/rpc.ts @@ -29,12 +29,14 @@ export type SandboxHealthCheck = () => Promise * - Bun: `The socket connection was closed unexpectedly` * - Deno: `error reading a body from connection` * - Cloudflare Workers: `Network connection lost` + * - Browser: `network error` */ const CONNECTION_TERMINATED_MESSAGES = [ 'terminated', 'The socket connection was closed unexpectedly', 'error reading a body from connection', 'Network connection lost', + 'network error', ] /** diff --git a/packages/js-sdk/src/utils.ts b/packages/js-sdk/src/utils.ts index 9db66d7bf1..a0c598d3a7 100644 --- a/packages/js-sdk/src/utils.ts +++ b/packages/js-sdk/src/utils.ts @@ -168,9 +168,22 @@ export async function toBlob( if (isBlobLike(data)) { return new Blob([await data.arrayBuffer()], { type: data.type }) } - // ReadableStream - must consume to get Blob + // ReadableStream - must consume to get Blob. Drained through the reader + // rather than `new Response(stream)`: the platform only accepts a body it + // recognizes, and what counts differs per runtime (undici takes any async + // iterable, a browser takes only its own stream class and stringifies the + // rest to "[object ReadableStream]"). The reader is the portable part. if (isReadableStreamLike(data)) { - return new Response(toDispatchableStream(data)).blob() + const reader = data.getReader() + const chunks: BlobPart[] = [] + for (;;) { + const { done, value } = await reader.read() + if (done) { + break + } + chunks.push(value) + } + return new Blob(chunks) } // String or ArrayBuffer - create Blob. A cross-realm ArrayBuffer needs no // special handling: buffer sources are recognized by V8, not by brand. diff --git a/packages/js-sdk/tests/api/http2.test.ts b/packages/js-sdk/tests/api/http2.test.ts index 262bf96bf3..3238e16b72 100644 --- a/packages/js-sdk/tests/api/http2.test.ts +++ b/packages/js-sdk/tests/api/http2.test.ts @@ -3,8 +3,6 @@ import { afterEach, expect, test, vi } from 'vitest' afterEach(() => { vi.restoreAllMocks() vi.resetModules() - vi.doUnmock('undici') - vi.doUnmock('../../src/utils') delete process.env.E2B_API_CONNECTIONS delete process.env.E2B_API_INFLIGHT_REQUESTS }) diff --git a/packages/js-sdk/tests/api/list.test.ts b/packages/js-sdk/tests/api/list.test.ts index 52cb8a9f24..e184b19eda 100644 --- a/packages/js-sdk/tests/api/list.test.ts +++ b/packages/js-sdk/tests/api/list.test.ts @@ -1,5 +1,4 @@ import { assert } from 'vitest' -import { randomUUID } from 'crypto' import { Sandbox, SandboxInfo } from '../../src' import { sandboxTest, isDebug } from '../setup.js' @@ -20,7 +19,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => { - const uniqueId = randomUUID() + const uniqueId = crypto.randomUUID() const extraSbx = await Sandbox.create({ metadata: { uniqueId } }) try { @@ -316,7 +315,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => { - const uniqueId = randomUUID() + const uniqueId = crypto.randomUUID() const extraSbx = await Sandbox.create({ metadata: { uniqueId } }) try { diff --git a/packages/js-sdk/tests/connectionConfig.test.ts b/packages/js-sdk/tests/connectionConfig.test.ts index df826d9692..b2f85f5ebf 100644 --- a/packages/js-sdk/tests/connectionConfig.test.ts +++ b/packages/js-sdk/tests/connectionConfig.test.ts @@ -4,6 +4,7 @@ import { setupRequestController, wrapStreamWithConnectionCleanup, } from '../src/connectionConfig' +import { runtime } from '../src/utils' // Store original env vars to restore after tests let originalEnv: { [key: string]: string | undefined } @@ -60,21 +61,26 @@ test('api_url has correct priority', () => { assert.equal(config.apiUrl, 'http://localhost:8080') }) -test('sandbox_url defaults to stable sandbox host in production', () => { - delete process.env.E2B_SANDBOX_URL - delete process.env.E2B_DOMAIN - delete process.env.E2B_DEBUG - - const config = new ConnectionConfig() - - assert.equal( - config.getSandboxUrl('sbx-test', { - sandboxDomain: 'e2b.app', - envdPort: 49983, - }), - 'https://sandbox.e2b.app' - ) -}) +// The stable host is deliberately not used in a browser (CORS); the browser +// side of this branch is asserted in connectionConfig.browser.test.ts. +test.skipIf(runtime === 'browser')( + 'sandbox_url defaults to stable sandbox host in production', + () => { + delete process.env.E2B_SANDBOX_URL + delete process.env.E2B_DOMAIN + delete process.env.E2B_DEBUG + + const config = new ConnectionConfig() + + assert.equal( + config.getSandboxUrl('sbx-test', { + sandboxDomain: 'e2b.app', + envdPort: 49983, + }), + 'https://sandbox.e2b.app' + ) + } +) test('sandbox_direct_url keeps per-sandbox host in production', () => { delete process.env.E2B_SANDBOX_URL diff --git a/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts b/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts index b886f3fe51..4140741946 100644 --- a/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts +++ b/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts @@ -126,6 +126,7 @@ describe('handleEnvdApiFetchError', () => { Bun: new Error('The socket connection was closed unexpectedly'), Deno: new TypeError('error reading a body from connection'), 'Cloudflare Workers': new Error('Network connection lost.'), + Browser: new TypeError('network error'), } for (const [runtime, error] of Object.entries(runtimeTerminatedErrors)) { diff --git a/packages/js-sdk/tests/envd/handleRpcError.test.ts b/packages/js-sdk/tests/envd/handleRpcError.test.ts index 8df1a6d6a6..f90f893e64 100644 --- a/packages/js-sdk/tests/envd/handleRpcError.test.ts +++ b/packages/js-sdk/tests/envd/handleRpcError.test.ts @@ -70,6 +70,7 @@ describe('handleRpcErrorWithHealthCheck', () => { Bun: 'The socket connection was closed unexpectedly', Deno: 'error reading a body from connection', 'Cloudflare Workers': 'Network connection lost.', + Browser: 'network error', } test('returns a TimeoutError when the health check says the sandbox is not running', async () => { diff --git a/packages/js-sdk/tests/envd/http2.test.ts b/packages/js-sdk/tests/envd/http2.test.ts index 9ad6e9ee0f..d1c022e891 100644 --- a/packages/js-sdk/tests/envd/http2.test.ts +++ b/packages/js-sdk/tests/envd/http2.test.ts @@ -3,8 +3,6 @@ import { afterEach, expect, test, vi } from 'vitest' afterEach(() => { vi.restoreAllMocks() vi.resetModules() - vi.doUnmock('undici') - vi.doUnmock('../../src/utils') delete process.env.E2B_ENVD_RPC_CONNECTIONS delete process.env.E2B_ENVD_INFLIGHT_REQUESTS delete process.env.E2B_ENVD_RPC_INFLIGHT_REQUESTS diff --git a/packages/js-sdk/tests/runtime.test.ts b/packages/js-sdk/tests/runtime.test.ts index af2919c7a3..a8d1aaf7d7 100644 --- a/packages/js-sdk/tests/runtime.test.ts +++ b/packages/js-sdk/tests/runtime.test.ts @@ -1,12 +1,13 @@ import { afterEach, expect, test, vi } from 'vitest' -// The unit project also runs under Bun, Deno, and Cloudflare's workerd, where -// the host runtime's own unstubbable marker (globalThis.Bun / globalThis.Deno / -// the Cloudflare-Workers user agent) correctly wins detection — these -// scenarios only exist on a Node host. +// The unit project also runs under Bun, Deno, Cloudflare's workerd, and a real +// browser, where the host runtime's own unstubbable marker (globalThis.Bun / +// globalThis.Deno / the Cloudflare-Workers user agent / `window`) correctly +// wins detection — these scenarios only exist on a Node host. const isNodeHost = typeof (globalThis as any).Bun === 'undefined' && typeof (globalThis as any).Deno === 'undefined' && + typeof (globalThis as any).window === 'undefined' && (globalThis as any).navigator?.userAgent !== 'Cloudflare-Workers' afterEach(() => { diff --git a/packages/js-sdk/tests/runtimes/browser/importMeta.d.ts b/packages/js-sdk/tests/runtimes/browser/importMeta.d.ts new file mode 100644 index 0000000000..b9a3c72f76 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/importMeta.d.ts @@ -0,0 +1,7 @@ +// Vite exposes the runner's `env` config on `import.meta.env`, which the +// browser suite reads instead of `process.env`. The canonical types live in +// `vite/client`, but vite is only a transitive dependency here (via vitest) and +// isn't resolvable from this package, so declare the shape the suite uses. +interface ImportMeta { + readonly env: Record +} diff --git a/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts b/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts new file mode 100644 index 0000000000..7c5237c3e6 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts @@ -0,0 +1,50 @@ +import { assert, test } from 'vitest' + +import { template } from '../../template' + +// Bundlers don't give browser apps a `process` unless asked to, so the SDK has +// to be fully configurable through explicit options — `getEnvVar` +// (src/api/metadata.ts) returns '' when `process` is missing rather than +// throwing. The rest of this suite runs against the `process.env` shim from +// processEnv.setup.ts, which would mask a regression that reintroduces a bare +// `process` read on the create path, so this test drops the shim entirely and +// drives a real sandbox the way a browser app has to. +// +// The SDK is imported dynamically inside the test, after the shim is gone: a +// static import is evaluated at collection time, while the shim is still +// installed, which would let a top-level `process` read anywhere in the module +// graph pass here and still crash a browser app on import. +// +// Config comes from `import.meta.env` (where vitest puts the config's `env`), +// which is also how a Vite app would hand its own build-time values over. +const apiKey = import.meta.env.E2B_API_KEY +const domain = import.meta.env.E2B_DOMAIN || undefined +const isDebug = import.meta.env.E2B_DEBUG !== undefined + +test.skipIf(isDebug)( + 'drives a sandbox with no process global', + async () => { + const shim = Reflect.getOwnPropertyDescriptor(globalThis, 'process') + Reflect.deleteProperty(globalThis, 'process') + assert.equal(typeof process, 'undefined') + + try { + const { Sandbox } = await import('../../../src') + const sandbox = await Sandbox.create(template, { apiKey, domain }) + + try { + await sandbox.files.write('hello.txt', 'Hello World') + assert.equal(await sandbox.files.read('hello.txt'), 'Hello World') + + const result = await sandbox.commands.run('echo "from the browser"') + assert.equal(result.exitCode, 0) + assert.equal(result.stdout.trim(), 'from the browser') + } finally { + await sandbox.kill() + } + } finally { + if (shim) Reflect.defineProperty(globalThis, 'process', shim) + } + }, + 60_000 +) diff --git a/packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts b/packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts new file mode 100644 index 0000000000..b59a0a9a46 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts @@ -0,0 +1,15 @@ +// Browsers have no `process`: `getEnvVar` (src/api/metadata.ts) resolves to '' +// there, so E2B_* can only reach a browser app through whatever shim its +// bundler injects. Provide that shim — the same thing workerd's nodejs_compat +// gives the Cloudflare suite for free — so the shared suites can configure the +// SDK from the environment like they do on every other runtime. +// +// `process.env` aliases Vite's `import.meta.env`, which is where vitest puts +// the config's `env` and what `vi.stubEnv` writes to in browser mode, so env +// reads and stubbing both behave as they do on Node. +// +// Only `env` is defined: `process.release` must stay absent or the SDK would +// detect the runtime as 'node' instead of 'browser'. Tests that need a +// browser without any `process` at all delete it themselves — see +// noProcessGlobal.test.ts. +Object.assign(globalThis, { process: { env: import.meta.env } }) diff --git a/packages/js-sdk/tests/runtimes/browser/run.test.tsx b/packages/js-sdk/tests/runtimes/browser/run.test.tsx deleted file mode 100644 index dda6780c35..0000000000 --- a/packages/js-sdk/tests/runtimes/browser/run.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { expect, inject, test } from 'vitest' -import { render } from 'vitest-browser-react' -import React from 'react' -import { useEffect, useState } from 'react' - -import { Sandbox } from '../../../src' -import { template } from '../../template' - -function E2BTest() { - const [text, setText] = useState() - - useEffect(() => { - const getText = async () => { - const sandbox = await Sandbox.create(template, { - apiKey: inject('E2B_API_KEY'), - domain: inject('E2B_DOMAIN'), - }) - - try { - await sandbox.commands.run('echo "Hello World" > hello.txt') - const content = await sandbox.files.read('hello.txt') - setText(content) - } finally { - await sandbox.kill() - } - } - - getText() - }, []) - - return
{text}
-} -test('browser test', async () => { - const screen = await render() - await expect - .element(screen.getByText('Hello World'), { timeout: 30_000 }) - .toBeInTheDocument() -}, 40_000) diff --git a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts new file mode 100644 index 0000000000..b27276bdaf --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts @@ -0,0 +1,77 @@ +import { playwright } from '@vitest/browser-playwright' +import { config } from 'dotenv' +import { defineConfig } from 'vitest/config' + +// Real env vars win over `.env`, matching dotenv's own precedence. +const env = { ...config().parsed, ...process.env } + +// Config the shared suites read from the environment; everything the SDK takes +// is `E2B_`-prefixed. Forwarded by name rather than as the whole environment, +// which would inline every host variable into JS served to the browser. +const testEnv = Object.fromEntries( + Object.entries(env).filter(([name]) => name.startsWith('E2B_')) +) as Record + +// Runs the unit + connectionConfig projects (same coverage as test:bun / +// test:deno / test:cf) inside a real Chromium via Playwright, against src. +// Nothing is skipped for being a browser: the suites that can't run here are +// Node-only rather than browser-hostile, and they're excluded below. Tests that +// read a response from a server inside the sandbox start a CORS-enabled one +// (`corsHttpServerCmd` in tests/setup.ts), the way a browser app's own server +// would be configured. +export default defineConfig({ + test: { + name: 'browser', + include: [ + 'tests/**/*.test.ts', + // Browser-only suite; tests/runtimes/** is otherwise excluded below. + 'tests/runtimes/browser/**/*.test.ts', + ], + exclude: [ + // Other runtimes' suites, which run under their own configs. This + // suite's own browser-only tests are re-included above; a new runtime + // directory needs adding here. + 'tests/runtimes/cloudflare*/**', + 'tests/template/**', + // Inspects the host-built dist/index.mjs via node:fs, which the browser + // can never see; the Node unit project keeps running it. + 'tests/bundle/**', + // Resolves the `undici`/`undici8` packages off `process.versions.node`. + // The browser never takes that path — `createRuntimeFetch` late-binds + // the global fetch outside Node — so there is nothing to cover here. + 'tests/undici.test.ts', + // These mock the API with msw's `setupServer`, whose `msw/node` entry + // pulls in node:http and can't be served to the browser. Porting them + // means `setupWorker` plus a service worker served from a public dir. + // Any new suite that imports `msw/node` belongs here — `grep -rl msw/node + // tests/` lists the full set (tests/template/** is already excluded). + 'tests/client.test.ts', + 'tests/sandbox/abortSignal.test.ts', + 'tests/sandbox/egressProxy.test.ts', + 'tests/sandbox/iam.test.ts', + 'tests/sandbox/lifecycleRequest.test.ts', + 'tests/sandbox/networkTransform.test.ts', + 'tests/secret/secret.test.ts', + 'tests/volume/file.test.ts', + 'tests/volume/volume.test.ts', + ], + globals: false, + testTimeout: 30_000, + // A real browser has no `process`; the setup file shims `process.env` onto + // `import.meta.env`, where vitest puts `env` below, so the shared suites + // can read their config the way they do on every other runtime. + setupFiles: ['tests/runtimes/browser/processEnv.setup.ts'], + env: testEnv, + browser: { + enabled: true, + // Defaults to `isCI`, so set it explicitly for local runs too. + headless: true, + // These tests drive the SDK and never render anything, so a failure + // screenshot is a picture of a blank page. Off by default it would write + // one PNG per failed test into .vitest-attachments/. + screenshotFailures: false, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}) diff --git a/packages/js-sdk/tests/sandbox/commands/kill.test.ts b/packages/js-sdk/tests/sandbox/commands/kill.test.ts index 4eddcc4460..036615169e 100644 --- a/packages/js-sdk/tests/sandbox/commands/kill.test.ts +++ b/packages/js-sdk/tests/sandbox/commands/kill.test.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest' -import { ProcessExitError } from '../../../src/index.js' +import { CommandExitError } from '../../../src/index.js' import { sandboxTest } from '../../setup.js' @@ -10,7 +10,7 @@ sandboxTest('kill process', async ({ sandbox }) => { await sandbox.commands.kill(pid) await expect(sandbox.commands.run(`kill -0 ${pid}`)).rejects.toThrowError( - ProcessExitError + CommandExitError ) }) diff --git a/packages/js-sdk/tests/sandbox/files/read.test.ts b/packages/js-sdk/tests/sandbox/files/read.test.ts index 85b8f45b4e..d8315402c8 100644 --- a/packages/js-sdk/tests/sandbox/files/read.test.ts +++ b/packages/js-sdk/tests/sandbox/files/read.test.ts @@ -38,11 +38,12 @@ sandboxTest('read file as stream', async ({ sandbox }) => { await sandbox.files.write(filename, content) const stream = await sandbox.files.read(filename, { format: 'stream' }) - const chunks: Uint8Array[] = [] + const decoder = new TextDecoder() + let readContent = '' for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) + readContent += decoder.decode(chunk, { stream: true }) } - const readContent = Buffer.concat(chunks).toString('utf-8') + readContent += decoder.decode() assert.equal(readContent, content) }) @@ -71,56 +72,9 @@ sandboxTest('read empty file in all formats', async ({ sandbox }) => { const stream = await sandbox.files.read(filename, { format: 'stream' }) expect(stream).toBeInstanceOf(ReadableStream) - const chunks: Uint8Array[] = [] + let bytesRead = 0 for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) + bytesRead += chunk.length } - expect(Buffer.concat(chunks).length).toBe(0) -}) - -sandboxTest('read file as stream', async ({ sandbox }) => { - const filename = 'test_read_stream.txt' - const content = 'Streamed read content. '.repeat(10_000) - - await sandbox.files.write(filename, content) - const stream = await sandbox.files.read(filename, { format: 'stream' }) - - const chunks: Uint8Array[] = [] - for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) - } - const readContent = Buffer.concat(chunks).toString('utf-8') - assert.equal(readContent, content) -}) - -sandboxTest('read non-existing file as stream', async ({ sandbox }) => { - const filename = 'non_existing_file.txt' - - await expect( - sandbox.files.read(filename, { format: 'stream' }) - ).rejects.toThrowError(FileNotFoundError) -}) - -sandboxTest('read empty file in all formats', async ({ sandbox }) => { - const filename = 'empty-file-formats.txt' - await sandbox.commands.run(`touch ${filename}`) - - const text = await sandbox.files.read(filename, { format: 'text' }) - expect(text).toBe('') - - const bytes = await sandbox.files.read(filename, { format: 'bytes' }) - expect(bytes).toBeInstanceOf(Uint8Array) - expect(bytes.length).toBe(0) - - const blob = await sandbox.files.read(filename, { format: 'blob' }) - expect(blob).toBeInstanceOf(Blob) - expect(blob.size).toBe(0) - - const stream = await sandbox.files.read(filename, { format: 'stream' }) - expect(stream).toBeInstanceOf(ReadableStream) - const chunks: Uint8Array[] = [] - for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) - } - expect(Buffer.concat(chunks).length).toBe(0) + expect(bytesRead).toBe(0) }) diff --git a/packages/js-sdk/tests/sandbox/files/write.test.ts b/packages/js-sdk/tests/sandbox/files/write.test.ts index 61f046cfe0..5116006e3b 100644 --- a/packages/js-sdk/tests/sandbox/files/write.test.ts +++ b/packages/js-sdk/tests/sandbox/files/write.test.ts @@ -1,4 +1,3 @@ -import path from 'path' import { assert } from 'vitest' import { WriteEntry } from '../../../src/sandbox/filesystem' @@ -111,7 +110,7 @@ sandboxTest('write multiple files', async ({ sandbox }) => { const file = files[i] const info = infos[i] - assert.equal(info.name, path.basename(file.path)) + assert.equal(info.name, file.path.split('/').pop()) assert.equal(info.path, file.path) assert.equal(info.type, 'file') @@ -206,7 +205,7 @@ sandboxTest('writeFiles with multiple files', async ({ sandbox }) => { const file = files[i] const info = infos[i] - assert.equal(info.name, path.basename(file.path)) + assert.equal(info.name, file.path.split('/').pop()) assert.equal(info.path, `/home/user/${file.path}`) assert.equal(info.type, 'file') diff --git a/packages/js-sdk/tests/sandbox/git/helpers.ts b/packages/js-sdk/tests/sandbox/git/helpers.ts index 797682f05b..cb19c1d82a 100644 --- a/packages/js-sdk/tests/sandbox/git/helpers.ts +++ b/packages/js-sdk/tests/sandbox/git/helpers.ts @@ -1,5 +1,3 @@ -import { randomUUID } from 'node:crypto' - export const AUTHOR_NAME = 'Sandbox Bot' export const AUTHOR_EMAIL = 'sandbox@example.com' export const USERNAME = 'git' @@ -10,7 +8,7 @@ export const PROTOCOL = 'https' const BASE_DIR = '/tmp/test-git' export async function createBaseDir(sandbox: any) { - const baseDir = `${BASE_DIR}/${randomUUID()}` + const baseDir = `${BASE_DIR}/${crypto.randomUUID()}` await sandbox.commands.run(`rm -rf "${baseDir}" && mkdir -p "${baseDir}"`) return baseDir } diff --git a/packages/js-sdk/tests/sandbox/host.test.ts b/packages/js-sdk/tests/sandbox/host.test.ts index 37e45f68c8..a6712536a1 100644 --- a/packages/js-sdk/tests/sandbox/host.test.ts +++ b/packages/js-sdk/tests/sandbox/host.test.ts @@ -1,11 +1,11 @@ import { assert } from 'vitest' -import { isDebug, sandboxTest, wait } from '../setup.js' +import { isDebug, sandboxTest, wait, corsHttpServerCmd } from '../setup.js' import { catchCmdExitErrorInBackground } from '../cmdHelper.js' sandboxTest( 'ping server in running sandbox', async ({ sandbox }) => { - const cmd = await sandbox.commands.run('python -m http.server 8000', { + const cmd = await sandbox.commands.run(corsHttpServerCmd(8000), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts index 28a1999b7a..c51c6344ce 100644 --- a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts +++ b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts @@ -1,7 +1,7 @@ import { assert, expect, test } from 'vitest' import { InvalidArgumentError, Sandbox } from '../../src' -import { isDebug, template, wait } from '../setup.js' +import { isDebug, template, wait, corsHttpServerCmd } from '../setup.js' test.skipIf(isDebug)( 'filesystem-only auto-pause cannot be combined with auto-resume', @@ -123,7 +123,7 @@ test.skipIf(isDebug)( }) try { - await sandbox.commands.run('python3 -m http.server 8000', { + await sandbox.commands.run(corsHttpServerCmd(8000), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/network.test.ts b/packages/js-sdk/tests/sandbox/network.test.ts index 82a8f20003..08fcc12d5d 100644 --- a/packages/js-sdk/tests/sandbox/network.test.ts +++ b/packages/js-sdk/tests/sandbox/network.test.ts @@ -1,7 +1,7 @@ import { assert, expect, describe } from 'vitest' import { CommandExitError, Sandbox } from '../../src' -import { sandboxTest, isDebug, template } from '../setup.js' +import { sandboxTest, isDebug, template, corsHttpServerCmd } from '../setup.js' import { httpbinTemplate } from '../template.js' describe('allow only 1.1.1.1', () => { @@ -138,7 +138,7 @@ describe('allowPublicTraffic=false', () => { // Start a simple HTTP server in the sandbox const port = 8080 - sandbox.commands.run(`python3 -m http.server ${port}`, { + sandbox.commands.run(corsHttpServerCmd(port), { background: true, }) @@ -177,7 +177,7 @@ describe('allowPublicTraffic=true', () => { async ({ sandbox }) => { // Start a simple HTTP server in the sandbox const port = 8080 - sandbox.commands.run(`python3 -m http.server ${port}`, { + sandbox.commands.run(corsHttpServerCmd(port), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/pty/kill.test.ts b/packages/js-sdk/tests/sandbox/pty/kill.test.ts index f602515770..d0693f7a41 100644 --- a/packages/js-sdk/tests/sandbox/pty/kill.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/kill.test.ts @@ -1,6 +1,6 @@ import { sandboxTest } from '../../setup' import { assert, expect } from 'vitest' -import { ProcessExitError } from '../../../src/index.js' +import { CommandExitError } from '../../../src/index.js' sandboxTest('kill PTY', async ({ sandbox }) => { const terminal = await sandbox.pty.create({ @@ -15,7 +15,7 @@ sandboxTest('kill PTY', async ({ sandbox }) => { // The PTY process should no longer be running. await expect( sandbox.commands.run(`kill -0 ${terminal.pid}`) - ).rejects.toThrowError(ProcessExitError) + ).rejects.toThrowError(CommandExitError) }) sandboxTest('kill non-existing PTY', async ({ sandbox }) => { diff --git a/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts b/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts index 903da6d105..6ba72d6b6f 100644 --- a/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts @@ -18,7 +18,7 @@ sandboxTest('pty connect/reconnect', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('echo $FOO\n')) + new TextEncoder().encode('echo $FOO\n') ) // Give time for the command output in the first connection @@ -35,7 +35,7 @@ sandboxTest('pty connect/reconnect', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('echo $FOO\nexit\n')) + new TextEncoder().encode('echo $FOO\nexit\n') ) await reconnectHandle.wait() diff --git a/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts b/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts index 5a0b98c589..d77db612c0 100644 --- a/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts @@ -17,7 +17,7 @@ sandboxTest('create PTY', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('echo $ABC\nexit\n')) + new TextEncoder().encode('echo $ABC\nexit\n') ) await terminal.wait() diff --git a/packages/js-sdk/tests/sandbox/pty/resize.test.ts b/packages/js-sdk/tests/sandbox/pty/resize.test.ts index 21bb878298..143182b6d8 100644 --- a/packages/js-sdk/tests/sandbox/pty/resize.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/resize.test.ts @@ -16,7 +16,7 @@ sandboxTest('resize', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('tput cols\nexit\n')) + new TextEncoder().encode('tput cols\nexit\n') ) await terminal.wait() @@ -33,7 +33,7 @@ sandboxTest('resize', async ({ sandbox }) => { await sandbox.pty.resize(resizedTerminal.pid, { cols: 100, rows: 24 }) await sandbox.pty.sendInput( resizedTerminal.pid, - new Uint8Array(Buffer.from('tput cols\nexit\n')) + new TextEncoder().encode('tput cols\nexit\n') ) await resizedTerminal.wait() diff --git a/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts b/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts index b32d7f23dd..c64c2fbee5 100644 --- a/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts @@ -8,10 +8,7 @@ sandboxTest('send input', async ({ sandbox }) => { onData: () => null, }) - await sandbox.pty.sendInput( - terminal.pid, - new Uint8Array(Buffer.from('exit\n')) - ) + await sandbox.pty.sendInput(terminal.pid, new TextEncoder().encode('exit\n')) await terminal.wait() expect(terminal.exitCode).toBe(0) diff --git a/packages/js-sdk/tests/sandbox/secure.test.ts b/packages/js-sdk/tests/sandbox/secure.test.ts index 90a8712941..e8647031fd 100644 --- a/packages/js-sdk/tests/sandbox/secure.test.ts +++ b/packages/js-sdk/tests/sandbox/secure.test.ts @@ -1,7 +1,20 @@ import { assert, test, describe } from 'vitest' import { getSignature, Sandbox } from '../../src' import { sandboxTest, isDebug } from '../setup' -import { randomUUID, createHash } from 'node:crypto' + +/** + * Recomputes the signature envd expects, independently of the SDK's own + * implementation. WebCrypto rather than node:crypto so the expectation is + * computed the same way on every runtime the suite covers. + */ +async function expectedSignature(raw: string): Promise { + const digest = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(raw) + ) + const base64 = btoa(String.fromCharCode(...new Uint8Array(digest))) + return 'v1_' + base64.replace(/=+$/, '') +} describe('secure sandbox', () => { sandboxTest.override({ @@ -40,16 +53,12 @@ test.skipIf(isDebug)('signing generation', async () => { const operation = 'read' const path = '/home/user/hello.txt' const user = 'root' - const envdAccessToken = randomUUID() + const envdAccessToken = crypto.randomUUID() const signatureRaw = `${path}:${operation}:${user}:${envdAccessToken}` - const buff = Buffer.from(signatureRaw, 'utf8') - const hash = createHash('sha256').update(buff).digest() - const signature = 'v1_' + hash.toString('base64').replace(/=+$/, '') - const readSignatureExpected = { - signature: signature, + signature: await expectedSignature(signatureRaw), expiration: null, } @@ -67,7 +76,7 @@ test.skipIf(isDebug)('signing generation with expiration', async () => { const operation = 'read' const path = '/home/user/hello.txt' const user = 'root' - const envdAccessToken = randomUUID() + const envdAccessToken = crypto.randomUUID() const expirationInSeconds = 120 const signatureExpiration = expirationInSeconds @@ -75,12 +84,8 @@ test.skipIf(isDebug)('signing generation with expiration', async () => { : null const signatureRaw = `${path}:${operation}:${user}:${envdAccessToken}:${signatureExpiration?.toString()}` - const buff = Buffer.from(signatureRaw, 'utf8') - const hash = createHash('sha256').update(buff).digest() - const signature = 'v1_' + hash.toString('base64').replace(/=+$/, '') - const readSignatureExpected = { - signature: signature, + signature: await expectedSignature(signatureRaw), expiration: signatureExpiration, } diff --git a/packages/js-sdk/tests/sandbox/snapshot.test.ts b/packages/js-sdk/tests/sandbox/snapshot.test.ts index ac0dead40d..47a85cfea1 100644 --- a/packages/js-sdk/tests/sandbox/snapshot.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot.test.ts @@ -1,6 +1,6 @@ import { assert, describe } from 'vitest' -import { sandboxTest, isDebug } from '../setup.js' +import { sandboxTest, isDebug, corsHttpServerCmd } from '../setup.js' sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox', @@ -140,7 +140,7 @@ sandboxTest.skipIf(isDebug)( sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with http server', async ({ sandbox }) => { - await sandbox.commands.run('python3 -m http.server 8000', { + await sandbox.commands.run(corsHttpServerCmd(8000), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/urls.test.ts b/packages/js-sdk/tests/sandbox/urls.test.ts index f20854e75f..7fa6819356 100644 --- a/packages/js-sdk/tests/sandbox/urls.test.ts +++ b/packages/js-sdk/tests/sandbox/urls.test.ts @@ -1,6 +1,7 @@ import { assert, describe, test } from 'vitest' import { getSignature, InvalidArgumentError, Sandbox } from '../../src' +import { runtime } from '../../src/utils' import { TEST_API_KEY } from '../setup' function createSandbox(envdAccessToken?: string) { @@ -19,7 +20,14 @@ describe('sandbox file URLs', () => { test('file URLs use direct sandbox host when envd API uses stable host', async () => { const sandbox = createSandbox() - assert.equal(sandbox['envdApiUrl'], 'https://sandbox.e2b.app') + // The stable host is deliberately not used in a browser (CORS), which + // reaches envd on the direct host — file URLs use the direct host either way. + assert.equal( + sandbox['envdApiUrl'], + runtime === 'browser' + ? 'https://49983-sandbox-id.e2b.app' + : 'https://sandbox.e2b.app' + ) assert.equal(sandbox['envdDirectUrl'], 'https://49983-sandbox-id.e2b.app') assert.equal( await sandbox.downloadUrl('/tmp/a.txt'), diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 91d427c3cc..fc977c0f86 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -148,6 +148,40 @@ export const isDebug = process.env.E2B_DEBUG !== undefined /** Placeholder API key with a valid format for tests that don't hit the API. */ export const TEST_API_KEY = `e2b_${'0'.repeat(40)}` +/** + * Command that serves the working directory on `port` with CORS enabled, for + * tests that read a response from a server running inside the sandbox. + * + * Use this instead of `python -m http.server`, which sends no CORS headers: a + * browser only exposes a cross-origin response to JS when the server opts in, + * so there the fetch fails as an opaque `TypeError: Failed to fetch` no matter + * what the server answered. Opting in is what a real browser app's own server + * does too, and it's only ever the user's server that has to — the responses + * the sandbox proxy synthesizes carry their own CORS headers (belt#2068; the + * traffic-token paths are still awaiting the orchestrator rollout, which is why + * `sandbox requires traffic access token` is red on the browser leg). + * + * `Access-Control-Allow-Headers` and `do_OPTIONS` cover requests that carry a + * custom header, which the browser preflights before sending. + */ +export function corsHttpServerCmd(port: number): string { + return `python3 -c " +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer + +class Handler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Headers', '*') + SimpleHTTPRequestHandler.end_headers(self) + + def do_OPTIONS(self): + self.send_response(204) + self.end_headers() + +ThreadingHTTPServer(('', ${port}), Handler).serve_forever() +"` +} + function generateRandomString(length: number = 8): string { return Math.random() .toString(36) diff --git a/packages/js-sdk/vitest.config.mts b/packages/js-sdk/vitest.config.mts index 63439b0112..77c2fba23c 100644 --- a/packages/js-sdk/vitest.config.mts +++ b/packages/js-sdk/vitest.config.mts @@ -1,5 +1,4 @@ import { defineConfig } from 'vitest/config' -import { playwright } from '@vitest/browser-playwright' import { config } from 'dotenv' const env = config() @@ -36,23 +35,6 @@ export default defineConfig({ }, }, }, - { - test: { - name: 'browser', - include: ['tests/runtimes/browser/**/*.{test,spec}.tsx'], - browser: { - enabled: true, - headless: true, - instances: [{ browser: 'chromium' }], - provider: playwright(), - // https://playwright.dev - }, - provide: { - E2B_API_KEY: process.env.E2B_API_KEY || env.parsed?.E2B_API_KEY, - E2B_DOMAIN: process.env.E2B_DOMAIN || env.parsed?.E2B_DOMAIN, - }, - }, - }, { test: { name: 'template', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33f613d52f..f8a9671b62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,27 +178,15 @@ importers: '@redocly/cli': specifier: 2.46.1 version: 2.46.1 - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/node': specifier: ^20.19.19 version: 20.19.43 '@types/platform': specifier: ^1.3.6 version: 1.3.6 - '@types/react': - specifier: ^19.2.18 - version: 19.2.18 - '@types/react-dom': - specifier: ^19.2.4 - version: 19.2.4(@types/react@19.2.18) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.3.4(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)) '@vitest/browser': specifier: ^4.1.10 version: 4.1.10(bufferutil@4.0.8)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))(vitest@4.1.10) @@ -229,12 +217,6 @@ importers: playwright: specifier: ^1.62.1 version: 1.62.1 - react: - specifier: ^19.2.8 - version: 19.2.8 - react-dom: - specifier: ^19.2.8 - version: 19.2.8(react@19.2.8) tsdown: specifier: ^0.22.14 version: 0.22.14(@typescript/typescript6@6.0.2) @@ -244,9 +226,6 @@ importers: vitest: specifier: ^4.1.10 version: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)) - vitest-browser-react: - specifier: ^2.2.0 - version: 2.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.10) wrangler: specifier: ^4.123.0 version: 4.123.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) @@ -267,119 +246,23 @@ packages: resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.7': - resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.7': - resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.29.7': - resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.29.7': - resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.29.7': - resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.29.7': - resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.7': - resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.25.8': - resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.27.2': - resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - - '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': - resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.25.8': - resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.1': - resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -1377,9 +1260,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1872,40 +1752,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} - engines: {node: '>=18'} - - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1943,14 +1789,6 @@ packages: '@types/platform@1.3.6': resolution: {integrity: sha512-ZmSaqHuvzv+jC232cFoz2QqPUkaj6EvMmCrWcx3WRr7xTPVFCMUOTcOq8m2d+Zw1iKRc1kDiaA+jtNrV0hkVew==} - '@types/react-dom@19.2.4': - resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.18': - resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} - '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -2084,12 +1922,6 @@ packages: resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} hasBin: true - '@vitejs/plugin-react@4.3.4': - resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^6.4.3 - '@vitest/browser-playwright@4.1.10': resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==} peerDependencies: @@ -2310,10 +2142,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -2331,9 +2159,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -2393,11 +2218,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2425,9 +2245,6 @@ packages: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} engines: {node: '>=14.16'} - caniuse-lite@1.0.30001753: - resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2533,9 +2350,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2575,10 +2389,6 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2594,9 +2404,6 @@ packages: dockerfile-ast@0.7.1: resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==} - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -2620,9 +2427,6 @@ packages: easy-table@1.2.0: resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} - electron-to-chromium@1.5.80: - resolution: {integrity: sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2789,10 +2593,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -3137,11 +2937,6 @@ packages: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true - jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} @@ -3160,11 +2955,6 @@ packages: json2md@2.0.3: resolution: {integrity: sha512-ZPzh6Djvqz8grJMxKllfCHo0p+p7BsbZ1J95KcCJgvvfdoy7myuKrrkUp80Kpy+wGauykC0dYljLqLY0kENaOw==} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -3202,13 +2992,6 @@ packages: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3313,9 +3096,6 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3546,10 +3326,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-ms@9.1.0: resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} engines: {node: '>=18'} @@ -3570,22 +3346,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.2.8: - resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} - peerDependencies: - react: ^19.2.8 - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} - engines: {node: '>=0.10.0'} - - react@19.2.8: - resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} - engines: {node: '>=0.10.0'} - read-pkg@3.0.0: resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} engines: {node: '>=4'} @@ -3685,17 +3445,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -3961,10 +3714,6 @@ packages: resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} hasBin: true - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4092,12 +3841,6 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - update-browserslist-db@1.1.2: - resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} @@ -4156,20 +3899,6 @@ packages: yaml: optional: true - vitest-browser-react@2.2.0: - resolution: {integrity: sha512-oY3KM6305kwJMa6nHo92vVtkOsih7mjEf12dLKuphaF+9ywWPEc+qanIBd394SZ6m5LadVEaG6dicvvizOzmjA==} - peerDependencies: - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - vitest: ^4.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - vitest@4.1.10: resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -4320,9 +4049,6 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4394,136 +4120,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.7': {} - - '@babel/core@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@10.2.2) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.0.2 - - '@babel/helper-compilation-targets@7.29.7': - dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.24.4 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-module-imports@7.29.7': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.26.5': {} - - '@babel/helper-string-parser@7.25.7': {} - - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.25.7': {} - - '@babel/helper-validator-identifier@7.27.1': {} - '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.29.7': {} - - '@babel/helpers@7.29.7': - dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/parser@7.25.8': - dependencies: - '@babel/types': 7.27.1 - - '@babel/parser@7.27.2': - dependencies: - '@babel/types': 7.27.1 - '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/runtime@7.29.7': {} - '@babel/template@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - '@babel/types@7.25.8': - dependencies: - '@babel/helper-string-parser': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - to-fast-properties: 2.0.0 - - '@babel/types@7.27.1': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -5316,11 +4922,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + optional: true '@jridgewell/resolve-uri@3.1.2': {} @@ -5668,50 +5270,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@testing-library/dom@10.4.0': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/runtime': 7.29.7 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - - '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@babel/runtime': 7.29.7 - '@testing-library/dom': 10.4.0 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@types/aria-query@5.0.4': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 - - '@types/babel__generator@7.6.8': - dependencies: - '@babel/types': 7.27.1 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 - - '@types/babel__traverse@7.20.6': - dependencies: - '@babel/types': 7.27.1 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5746,14 +5304,6 @@ snapshots: '@types/platform@1.3.6': {} - '@types/react-dom@19.2.4(@types/react@19.2.18)': - dependencies: - '@types/react': 19.2.18 - - '@types/react@19.2.18': - dependencies: - csstype: 3.2.3 - '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 20.19.43 @@ -5828,17 +5378,6 @@ snapshots: dependencies: '@typescript/old': typescript@6.0.3 - '@vitejs/plugin-react@4.3.4(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))': - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.29.7) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0) - transitivePeerDependencies: - - supports-color - '@vitest/browser-playwright@4.1.10(bufferutil@4.0.8)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(playwright@1.62.1)(utf-8-validate@6.0.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(bufferutil@4.0.8)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))(vitest@4.1.10) @@ -6029,8 +5568,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} ansis@4.3.1: {} @@ -6043,10 +5580,6 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -6116,13 +5649,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.24.4: - dependencies: - caniuse-lite: 1.0.30001753 - electron-to-chromium: 1.5.80 - node-releases: 2.0.19 - update-browserslist-db: 1.1.2(browserslist@4.24.4) - buffer-from@1.1.2: optional: true @@ -6152,8 +5678,6 @@ snapshots: camelcase@7.0.1: {} - caniuse-lite@1.0.30001753: {} - chai@6.2.2: {} chalk@2.4.2: @@ -6250,8 +5774,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - csstype@3.2.3: {} - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -6301,8 +5823,6 @@ snapshots: defu@6.1.7: {} - dequal@2.0.3: {} - detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -6316,8 +5836,6 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - dom-accessibility-api@0.5.16: {} - dotenv@16.4.5: {} dts-resolver@3.0.0: {} @@ -6336,8 +5854,6 @@ snapshots: optionalDependencies: wcwidth: 1.0.1 - electron-to-chromium@1.5.80: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -6604,8 +6120,6 @@ snapshots: functions-have-names@1.2.3: {} - gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} get-intrinsic@1.2.1: @@ -6940,8 +6454,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.0.2: {} - json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@5.0.0: {} @@ -6964,8 +6476,6 @@ snapshots: dependencies: indento: 1.1.13 - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -7012,12 +6522,6 @@ snapshots: lru-cache@11.5.2: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7138,8 +6642,6 @@ snapshots: node-gyp-build@4.8.4: optional: true - node-releases@2.0.19: {} - normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -7354,12 +6856,6 @@ snapshots: prettier@3.6.2: {} - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - pretty-ms@9.1.0: dependencies: parse-ms: 4.0.0 @@ -7374,17 +6870,6 @@ snapshots: queue-microtask@1.2.3: {} - react-dom@19.2.8(react@19.2.8): - dependencies: - react: 19.2.8 - scheduler: 0.27.0 - - react-is@17.0.2: {} - - react-refresh@0.14.2: {} - - react@19.2.8: {} - read-pkg@3.0.0: dependencies: load-json-file: 4.0.0 @@ -7538,12 +7023,8 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.27.0: {} - semver@5.7.2: {} - semver@6.3.1: {} - semver@7.7.2: {} semver@7.8.5: {} @@ -7870,8 +7351,6 @@ snapshots: dependencies: tldts-core: 7.4.10 - to-fast-properties@2.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -8009,12 +7488,6 @@ snapshots: until-async@3.0.2: {} - update-browserslist-db@1.1.2(browserslist@4.24.4): - dependencies: - browserslist: 4.24.4 - escalade: 3.2.0 - picocolors: 1.1.1 - uri-js-replace@1.0.1: {} utf-8-validate@6.0.3: @@ -8045,15 +7518,6 @@ snapshots: jiti: 2.4.2 terser: 5.46.0 - vitest-browser-react@2.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.10): - dependencies: - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - vitest: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - vitest@4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)): dependencies: '@vitest/expect': 4.1.10 @@ -8230,8 +7694,6 @@ snapshots: y18n@5.0.8: {} - yallist@3.1.1: {} - yallist@5.0.0: {} yaml-ast-parser@0.0.43: {}