diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5386b06..1e169f1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,16 +8,16 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: '1.25.0' + go-version-file: go.mod - run: yarn install --frozen-lockfile - run: yarn lint - - run: yarn cucumber + - run: go test ./... env: DOCKER_HOST: unix:///var/run/docker.sock build: diff --git a/Dockerfile b/Dockerfile index aa6a505..a9a7c6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25.1-alpine@sha256:b6ed3fd0452c0e9bcdef5597f29cc1418f61672e9d3a2f55bf02e7222c014abd AS builder +FROM golang:1.26.2-alpine@sha256:f85330846cde1e57ca9ec309382da3b8e6ae3ab943d2739500e08c86393a21b1 AS builder WORKDIR /netlib diff --git a/cmd/testproxy/main.go b/cmd/testproxy/main.go index e4616fb..0e221f2 100644 --- a/cmd/testproxy/main.go +++ b/cmd/testproxy/main.go @@ -39,12 +39,16 @@ func main() { interrupts := make(map[string]bool) http.HandleFunc("/create", func(w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") + host := r.FormValue("host") + if host == "" { + host = "127.0.0.1" + } port := r.FormValue("port") laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") if err != nil { panic(err) } - raddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:"+port) + raddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, port)) if err != nil { panic(err) } @@ -113,7 +117,7 @@ func main() { addr := util.Getenv("ADDR", ":8080") server := &http.Server{ Addr: addr, - Handler: http.DefaultServeMux, + Handler: withCORS(http.DefaultServeMux), } go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { @@ -130,3 +134,16 @@ func main() { logger.Fatal("failed to shutdown server", zap.Error(err)) } } + +func withCORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/features/browser-harness.ts b/features/browser-harness.ts new file mode 100644 index 0000000..aa4d5e8 --- /dev/null +++ b/features/browser-harness.ts @@ -0,0 +1,357 @@ +import { Network } from '../lib' +import { LobbyListEntry, PeerConfiguration } from '../lib/types' + +export const browserHarnessLoaded = true + +declare global { + interface Window { + netlibTest: any + netlibTestReady: boolean + netlibTestLoadErrors?: string[] + } +} + +interface RecordedEvent { + eventName: string + eventPayload: any[] +} + +interface PlayerOptions { + gameID: string + signalingURL: string + testproxyURL?: string +} + +const allEvents = ['close', 'ready', 'lobby', 'left', 'connected', 'disconnected', 'reconnecting', 'reconnected', 'message', 'signalingerror', 'signalingreconnected', 'leader', 'lobbyUpdated'] + +let network: Network | undefined +let events: RecordedEvent[] = [] +let scanIndex = 0 +let lastReceivedLobbies: LobbyListEntry[] = [] +let lastError: string | null = null + +function requireNetwork (): Network { + if (network === undefined) { + throw new Error('network has not been created') + } + return network +} + +function serializeArg (arg: any): any { + if (arg === undefined) { + return null + } + if (arg === null) { + return null + } + if (typeof arg === 'object') { + if (!Array.isArray(arg)) { + const text = String(arg) + if (text !== '[object Object]') { + return { __netlibString: text } + } + } + try { + return JSON.parse(JSON.stringify(arg)) + } catch (_) { + return { __netlibString: String(arg) } + } + } + return arg +} + +function stringifyArg (arg: any): string { + if (arg !== null && typeof arg === 'object' && typeof arg.__netlibString === 'string') { + return arg.__netlibString + } + return String(arg) +} + +function normalizeComparable (arg: any): any { + if (arg === null || typeof arg !== 'object') { + return arg + } + if (Array.isArray(arg)) { + return arg.map(normalizeComparable) + } + const copy: Record = {} + Object.keys(arg) + .filter(key => key !== 'createdAt' && key !== 'updatedAt') + .sort() + .forEach(key => { + copy[key] = normalizeComparable(arg[key]) + }) + return copy +} + +function debugArg (arg: any): string { + if (arg !== null && typeof arg === 'object') { + try { + return JSON.stringify(normalizeComparable(arg)) + } catch (_) { + return stringifyArg(arg) + } + } + return stringifyArg(arg) +} + +function matchEvent (event: RecordedEvent, eventName: string, matchArguments: any[] = []): boolean { + if (event.eventName !== eventName) { + return false + } + let argumentsMatch = true + for (let i = 0; i < matchArguments.length; i++) { + const expected = matchArguments[i] + const actual = event.eventPayload[i] + if (typeof expected === 'string' || expected instanceof String) { + argumentsMatch = stringifyArg(actual) === expected + } else if (actual !== null && typeof actual === 'object') { + argumentsMatch = JSON.stringify(normalizeComparable(actual)) === JSON.stringify(normalizeComparable(expected)) + } else if (actual !== expected) { + argumentsMatch = false + } else { + argumentsMatch = true + } + } + return argumentsMatch +} + +function findEvent (eventName: string, matchArguments: any[] = [], fromScanIndex: boolean = false): RecordedEvent | null { + const offset = fromScanIndex ? scanIndex : 0 + const ix = events.slice(offset).findIndex(event => matchEvent(event, eventName, matchArguments)) + if (ix < 0) { + return null + } + return events[offset + ix] +} + +function consumeEvent (event: RecordedEvent): void { + const ix = events.slice(scanIndex).indexOf(event) + if (ix >= 0) { + scanIndex += ix + 1 + } +} + +async function delay (ms: number): Promise { + return await new Promise(resolve => setTimeout(resolve, ms)) +} + +function sdpCandidateLines (sdp?: string): string[] { + return (sdp ?? '').split('\r\n').filter(line => line.startsWith('a=candidate')) +} + +function peerDiagnostics (): any[] { + if (network === undefined) { + return [] + } + return Array.from(network.peers.entries()).map(([id, peer]: [string, any]) => ({ + id, + connectionState: peer.conn.connectionState, + iceConnectionState: peer.conn.iceConnectionState, + iceGatheringState: peer.conn.iceGatheringState, + signalingState: peer.conn.signalingState, + localCandidates: sdpCandidateLines(peer.conn.localDescription?.sdp), + remoteCandidates: sdpCandidateLines(peer.conn.remoteDescription?.sdp) + })) +} + +async function waitForEvent (eventName: string, matchArguments: any[] = [], consume: boolean = true, timeoutMs: number = 20000): Promise { + if (!allEvents.includes(eventName)) { + throw new Error(`Event type ${eventName} not tracked, add to allEvents in browser-harness.ts`) + } + + const deadline = performance.now() + timeoutMs + for (;;) { + const event = findEvent(eventName, matchArguments, true) + if (event !== null) { + if (consume) { + consumeEvent(event) + } + return event + } + if (performance.now() >= deadline) { + const sameEvents = events.slice(scanIndex) + .filter(event => event.eventName === eventName) + .map(event => event.eventPayload.map(arg => debugArg(arg)).join(',')) + .join(' + ') + throw new Error(`Event not found, timed out, got: ${sameEvents}; peers: ${JSON.stringify(peerDiagnostics())}`) + } + await delay(100) + } +} + +function registerEvents (n: Network): void { + allEvents.forEach(eventName => { + n.on(eventName as any, (...args: any[]) => { + events.push({ + eventName, + eventPayload: args.map(serializeArg) + }) + }) + }) + + n.on('signalingerror', _ => {}) + n.on('rtcerror', _ => {}) +} + +async function createPlayer (options: PlayerOptions): Promise { + events = [] + scanIndex = 0 + lastReceivedLobbies = [] + lastError = null + + const config: PeerConfiguration = { + iceServers: [] + } + if (options.testproxyURL !== undefined && options.testproxyURL !== '') { + config.testproxyURL = options.testproxyURL + } + + network = new Network(options.gameID, config, options.signalingURL) + registerEvents(network) + + // Keep parity with the old Cucumber world, which gave the signaling socket a + // short head start before the caller waited for concrete events. + await delay(50) +} + +async function createLobby (settings?: any): Promise { + return await requireNetwork().create(settings) +} + +async function joinLobby (lobby: string, password?: string): Promise { + lastError = null + return await requireNetwork().join(lobby, password) +} + +async function tryJoinLobby (lobby: string, password?: string): Promise<{ ok: boolean, message?: string }> { + try { + await joinLobby(lobby, password) + return { ok: true } + } catch (e) { + lastError = e !== null && typeof e === 'object' && typeof (e as any).message === 'string' ? (e as any).message : String(e) + return { ok: false, message: lastError } + } +} + +async function listLobbies (filter?: object, sort?: object, limit?: number): Promise { + filter = filter ?? undefined + sort = sort ?? undefined + limit = limit ?? undefined + lastReceivedLobbies = await requireNetwork().list(filter, sort, limit) + return lastReceivedLobbies +} + +async function setLobbySettings (settings: any): Promise { + const result = await requireNetwork().setLobbySettings(settings) + if (result !== true) { + throw result + } +} + +function broadcast (channel: string, data: string): void { + requireNetwork().broadcast(channel, data) +} + +async function closeNetwork (reason?: string): Promise { + const n = requireNetwork() as any + const ws = n.signaling?.ws + const closed = new Promise(resolve => { + if (ws === undefined || ws.readyState === WebSocket.CLOSED) { + resolve() + return + } + ws.addEventListener('close', () => resolve(), { once: true }) + }) + requireNetwork().close(reason) + await Promise.race([closed, delay(100)]) +} + +async function leaveLobby (): Promise { + await requireNetwork().leave() +} + +function closeSignalingSocket (): void { + const n = requireNetwork() as any + n.signaling.ws.close() +} + +function forceReconnectSignaling (): void { + requireNetwork()._forceReconnectSignaling() +} + +function disableTestProxy (): void { + requireNetwork().peers.forEach(peer => { + peer.config.testproxyURL = undefined + }) +} + +function uninterruptOnConnectionState (otherPeerID: string, state: string, testproxyURL: string): void { + const n = requireNetwork() + const peer = n.peers.get(otherPeerID) + if (peer === undefined) { + throw new Error(`peer ${otherPeerID} not found`) + } + const uninterrupt = (): void => { + fetch(`${testproxyURL}/uninterrupt?id=${n.id + otherPeerID}`).then(() => {}).catch(console.error) + fetch(`${testproxyURL}/uninterrupt?id=${otherPeerID + n.id}`).then(() => {}).catch(console.error) + } + const onConnectionStateChange = (): void => { + if (peer.conn.connectionState === state) { + peer.conn.removeEventListener('connectionstatechange', onConnectionStateChange) + uninterrupt() + } + } + peer.conn.addEventListener('connectionstatechange', onConnectionStateChange) + onConnectionStateChange() +} + +function getID (): string { + return requireNetwork().id +} + +function getPeerCount (): number { + return requireNetwork().peers.size +} + +function getCurrentLobby (): string { + return requireNetwork().currentLobby ?? '' +} + +function getCurrentLeader (): string { + return requireNetwork().currentLeader ?? '' +} + +function getLastReceivedLobbies (): LobbyListEntry[] { + return lastReceivedLobbies +} + +function getLastError (): string { + return lastError ?? '' +} + +window.netlibTest = { + createPlayer, + createLobby, + joinLobby, + tryJoinLobby, + listLobbies, + setLobbySettings, + broadcast, + closeNetwork, + leaveLobby, + closeSignalingSocket, + forceReconnectSignaling, + disableTestProxy, + uninterruptOnConnectionState, + waitForEvent, + findEvent, + findNewEvent: (eventName: string, matchArguments: any[] = []) => findEvent(eventName, matchArguments, true), + getID, + getPeerCount, + getCurrentLobby, + getCurrentLeader, + getLastReceivedLobbies, + getLastError +} +window.netlibTestReady = true diff --git a/features/browser_features_test.go b/features/browser_features_test.go new file mode 100644 index 0000000..e946c5b --- /dev/null +++ b/features/browser_features_test.go @@ -0,0 +1,888 @@ +package features + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/exec" + "path/filepath" + goruntime "runtime" + "sort" + "strings" + "sync" + "syscall" + "testing" + "time" + + "github.com/chromedp/chromedp" + "github.com/cucumber/godog" + "github.com/cucumber/godog/colors" + + cdpRuntime "github.com/chromedp/cdproto/runtime" + messages "github.com/cucumber/messages/go/v21" +) + +const ( + defaultActionTimeout = 30 * time.Second + waitEventTimeout = 20 * time.Second +) + +type browserFeatureSuite struct { + rootDir string + tmpDir string + workDir string + + signalingBinary string + testproxyBinary string + + harnessBundle string + harnessServer *httptest.Server + harnessURL string + + allocCancel context.CancelFunc + browserCtx context.Context + browserCancel context.CancelFunc +} + +func TestMain(m *testing.M) { + if os.Getenv("SKIP_FEATURE_TESTS") == "true" { + os.Exit(m.Run()) + } + + opts := godog.Options{ + Format: "pretty", + Paths: []string{"."}, + Output: colors.Colored(os.Stdout), + Randomize: -1, + Strict: true, + } + godog.BindCommandLineFlags("godog.", &opts) + flag.Parse() + if len(flag.Args()) > 0 { + opts.Paths = flag.Args() + } + + suite, err := newBrowserFeatureSuite() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + world := newScenarioWorld(suite) + testSuite := godog.TestSuite{ + Name: "netlib", + ScenarioInitializer: world.InitializeScenario, + Options: &opts, + } + + status := testSuite.Run() + suite.Close() + os.Exit(status) +} + +func newBrowserFeatureSuite() (*browserFeatureSuite, error) { + rootDir, err := findRepoRoot() + if err != nil { + return nil, err + } + + tmpDir, err := os.MkdirTemp("", "netlib-browser-features-*") + if err != nil { + return nil, err + } + + suite := &browserFeatureSuite{ + rootDir: rootDir, + tmpDir: tmpDir, + } + + if err := suite.buildBackendBinaries(); err != nil { + suite.Close() + return nil, err + } + if err := suite.buildHarness(); err != nil { + suite.Close() + return nil, err + } + if err := suite.startHarnessServer(); err != nil { + suite.Close() + return nil, err + } + if err := suite.startChrome(); err != nil { + suite.Close() + return nil, err + } + + return suite, nil +} + +func findRepoRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", errors.New("could not find go.mod") + } + dir = parent + } +} + +func (s *browserFeatureSuite) buildBackendBinaries() error { + binaries := map[string]*string{ + "signaling": &s.signalingBinary, + "testproxy": &s.testproxyBinary, + } + for backend, target := range binaries { + output := filepath.Join(s.tmpDir, "netlib-cucumber-"+backend) + cmd := exec.Command("go", "build", "-o", output, "./cmd/"+backend) + cmd.Dir = s.rootDir + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("build %s: %w\n%s", backend, err, string(out)) + } + *target = output + } + return nil +} + +func (s *browserFeatureSuite) buildHarness() error { + distDir := filepath.Join(s.tmpDir, "harness") + workDir, err := os.MkdirTemp(s.tmpDir, "work-") + if err != nil { + return err + } + s.workDir = workDir + + repoLink := filepath.Join(workDir, "repo") + if err := os.Symlink(s.rootDir, repoLink); err != nil { + return err + } + harnessSource := "./repo/features/browser-harness.ts" + + if err := os.WriteFile(filepath.Join(workDir, "package.json"), []byte(`{"name":"netlib-browser-features","private":true}`+"\n"), 0o644); err != nil { + return err + } + indexHTML := ` + + + + +` + "\n" + if err := os.WriteFile(filepath.Join(workDir, "index.html"), []byte(indexHTML), 0o644); err != nil { + return err + } + harnessEntrypoint := "import { browserHarnessLoaded } from '" + harnessSource + "'\n" + + ";(window as any).netlibBrowserHarnessLoaded = browserHarnessLoaded\n" + if err := os.WriteFile(filepath.Join(workDir, "harness.ts"), []byte(harnessEntrypoint), 0o644); err != nil { + return err + } + + cacheDir := filepath.Join(s.tmpDir, "parcel-cache") + parcel := filepath.Join(s.rootDir, "node_modules", ".bin", "parcel") + cmd := exec.Command( + parcel, "build", "index.html", + "--dist-dir", distDir, + "--cache-dir", cacheDir, + "--no-content-hash", + "--no-source-maps", + "--no-optimize", + "--no-cache", + "--log-level", "error", + ) + cmd.Dir = workDir + cmd.Env = append(os.Environ(), "NODE_ENV=test") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("build browser harness: %w\n%s", err, string(out)) + } + + entries, err := os.ReadDir(distDir) + if err != nil { + return err + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".html") { + continue + } + if s.harnessBundle != "" { + return fmt.Errorf("expected one harness bundle, found at least %s and %s", filepath.Base(s.harnessBundle), entry.Name()) + } + s.harnessBundle = filepath.Join(distDir, entry.Name()) + } + if s.harnessBundle == "" { + return fmt.Errorf("browser harness build did not produce an HTML bundle in %s", distDir) + } + return nil +} + +func (s *browserFeatureSuite) startHarnessServer() error { + distDir := filepath.Dir(s.harnessBundle) + files := http.FileServer(http.Dir(distDir)) + s.harnessServer = httptest.NewServer(files) + s.harnessURL = s.harnessServer.URL + "/" + filepath.Base(s.harnessBundle) + return nil +} + +func (s *browserFeatureSuite) startChrome() error { + chromePath, err := findChromeExecutable() + if err != nil { + return err + } + + allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), chromeExecAllocatorOptions(chromePath)...) + browserCtx, browserCancel := chromedp.NewContext(allocCtx) + s.allocCancel = allocCancel + s.browserCtx = browserCtx + s.browserCancel = browserCancel + + return chromedp.Run(browserCtx) +} + +func chromeExecAllocatorOptions(chromePath string) []chromedp.ExecAllocatorOption { + disableFeatures := strings.Join([]string{ + "site-per-process", + "Translate", + "BlinkGenPropertyTrees", + "WebRtcHideLocalIpsWithMdns", + }, ",") + + opts := append([]chromedp.ExecAllocatorOption{}, chromedp.DefaultExecAllocatorOptions[:]...) + opts = append(opts, + chromedp.ExecPath(chromePath), + chromedp.WindowSize(1280, 720), + chromedp.Flag("disable-features", disableFeatures), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("autoplay-policy", "no-user-gesture-required"), + chromedp.Flag("force-webrtc-ip-handling-policy", "default"), + chromedp.Flag("remote-debugging-address", "127.0.0.1"), + ) + return opts +} + +func findChromeExecutable() (string, error) { + if chromePath := strings.TrimSpace(os.Getenv("CHROME_PATH")); chromePath != "" { + return resolveConfiguredChromePath("CHROME_PATH", chromePath) + } + + for _, name := range []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome"} { + if executable, err := exec.LookPath(name); err == nil { + return executable, nil + } + } + + if goruntime.GOOS == "darwin" { + for _, root := range chromeApplicationRoots() { + for _, appName := range []string{ + "Google Chrome.app", + "Google Chrome for Testing.app", + "Google Chrome Beta.app", + "Google Chrome Dev.app", + "Google Chrome Canary.app", + "Chromium.app", + } { + if executable, ok := resolveChromePath(filepath.Join(root, appName)); ok { + return executable, nil + } + } + } + } + + return "", errors.New("could not find Chrome; set CHROME_PATH to the Chrome executable or .app path") +} + +func resolveConfiguredChromePath(envName, chromePath string) (string, error) { + if executable, ok := resolveChromePath(chromePath); ok { + return executable, nil + } + if !strings.ContainsRune(chromePath, filepath.Separator) { + if resolved, err := exec.LookPath(chromePath); err == nil { + if executable, ok := resolveChromePath(resolved); ok { + return executable, nil + } + } + } + return "", fmt.Errorf("%s=%q does not point to an executable Chrome", envName, chromePath) +} + +func chromeApplicationRoots() []string { + roots := []string{"/Applications"} + if home, err := os.UserHomeDir(); err == nil && home != "" { + roots = append(roots, filepath.Join(home, "Applications")) + } + return roots +} + +func resolveChromePath(chromePath string) (string, bool) { + chromePath = strings.TrimSpace(chromePath) + if chromePath == "" { + return "", false + } + if strings.HasSuffix(chromePath, ".app") { + return executableInAppBundle(chromePath) + } + + info, err := os.Stat(chromePath) + if err != nil { + return "", false + } + if info.IsDir() { + return executableInAppBundle(chromePath) + } + if isExecutableFile(chromePath) { + return chromePath, true + } + return "", false +} + +func executableInAppBundle(appPath string) (string, bool) { + macOSDir := filepath.Join(appPath, "Contents", "MacOS") + baseName := strings.TrimSuffix(filepath.Base(appPath), ".app") + candidates := []string{filepath.Join(macOSDir, baseName)} + + entries, err := os.ReadDir(macOSDir) + if err == nil { + for _, entry := range entries { + if entry.IsDir() { + continue + } + candidate := filepath.Join(macOSDir, entry.Name()) + if candidate != candidates[0] { + candidates = append(candidates, candidate) + } + } + } + + for _, candidate := range candidates { + if isExecutableFile(candidate) { + return candidate, true + } + } + return "", false +} + +func isExecutableFile(path string) bool { + info, err := os.Stat(path) + return err == nil && info.Mode().IsRegular() && info.Mode().Perm()&0111 != 0 +} + +func (s *browserFeatureSuite) Close() { + if s.browserCancel != nil { + s.browserCancel() + } + if s.allocCancel != nil { + s.allocCancel() + } + if s.harnessServer != nil { + s.harnessServer.Close() + } + if s.tmpDir != "" { + _ = os.RemoveAll(s.tmpDir) + } + if s.workDir != "" { + _ = os.RemoveAll(s.workDir) + } +} + +type scenarioWorld struct { + suite *browserFeatureSuite + + stepArg *messages.PickleStepArgument + + backends map[string]*backendProcess + signalingURL string + testproxyURL string + databaseURL string + + useTestProxy bool + players map[string]*browserPlayer + lastError map[string]string +} + +func newScenarioWorld(suite *browserFeatureSuite) *scenarioWorld { + return &scenarioWorld{ + suite: suite, + } +} + +func (w *scenarioWorld) InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + w.reset() + return ctx, nil + }) + ctx.BeforeStep(func(st *godog.Step) { + w.stepArg = st.Argument + }) + ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { + return ctx, w.cleanup() + }) + + w.registerSteps(ctx) +} + +func (w *scenarioWorld) reset() { + w.stepArg = nil + w.backends = make(map[string]*backendProcess) + w.signalingURL = "" + w.testproxyURL = "" + w.databaseURL = "" + w.useTestProxy = false + w.players = make(map[string]*browserPlayer) + w.lastError = make(map[string]string) +} + +func (w *scenarioWorld) cleanup() error { + var errs []error + for _, player := range w.players { + if err := player.Close(); err != nil { + errs = append(errs, err) + } + } + w.players = make(map[string]*browserPlayer) + + keys := make([]string, 0, len(w.backends)) + for key := range w.backends { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if err := w.backends[key].Close(); err != nil { + errs = append(errs, fmt.Errorf("%s: %w", key, err)) + } + } + w.backends = make(map[string]*backendProcess) + return errors.Join(errs...) +} + +type browserPlayer struct { + name string + ctx context.Context + cancel context.CancelFunc +} + +func (w *scenarioWorld) newBrowserPlayer(name string) (*browserPlayer, error) { + tabCtx, cancel := chromedp.NewContext(w.suite.browserCtx) + player := &browserPlayer{ + name: name, + ctx: tabCtx, + cancel: cancel, + } + + chromedp.ListenTarget(tabCtx, func(ev any) { + switch ev := ev.(type) { + case *cdpRuntime.EventConsoleAPICalled: + var parts []string + for _, arg := range ev.Args { + if len(arg.Value) > 0 { + parts = append(parts, string(arg.Value)) + } else { + parts = append(parts, arg.Description) + } + } + if len(parts) > 0 { + _, _ = fmt.Fprintf(os.Stderr, "chrome[%s] console: %s\n", name, strings.Join(parts, " ")) + } + case *cdpRuntime.EventExceptionThrown: + _, _ = fmt.Fprintf(os.Stderr, "chrome[%s] exception: %s\n", name, ev.ExceptionDetails.Text) + } + }) + + if err := chromedp.Run(tabCtx); err != nil { + cancel() + return nil, err + } + + if err := player.run(defaultActionTimeout, + chromedp.Navigate(w.suite.harnessURL), + chromedp.WaitReady("body", chromedp.ByQuery), + ); err != nil { + cancel() + return nil, err + } + if err := player.waitForHarness(); err != nil { + cancel() + return nil, err + } + return player, nil +} + +func (p *browserPlayer) waitForHarness() error { + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + var ready bool + err := p.run(2*time.Second, chromedp.Evaluate(`window.netlibTestReady === true`, &ready)) + if err == nil && ready { + return nil + } + time.Sleep(100 * time.Millisecond) + } + diagnostics, err := p.harnessDiagnostics() + if err != nil { + return fmt.Errorf("browser harness did not become ready; diagnostics failed: %w", err) + } + return fmt.Errorf("browser harness did not become ready: %s", diagnostics) +} + +func (p *browserPlayer) harnessDiagnostics() (string, error) { + var diagnostics struct { + Href string `json:"href"` + Ready any `json:"ready"` + TestType string `json:"testType"` + Errors []string `json:"errors"` + BodyText string `json:"bodyText"` + ScriptSrc []string `json:"scriptSrc"` + } + err := p.run(2*time.Second, chromedp.Evaluate(`(() => ({ + href: window.location.href, + ready: window.netlibTestReady, + testType: typeof window.netlibTest, + errors: window.netlibTestLoadErrors || [], + bodyText: document.body ? document.body.innerText : "", + scriptSrc: Array.from(document.scripts).map(script => script.src || "") +}))()`, &diagnostics)) + if err != nil { + return "", err + } + return compactJSON(diagnostics), nil +} + +func (p *browserPlayer) call(name string, result any, args ...any) error { + if args == nil { + args = []any{} + } + argsJSON, err := json.Marshal(args) + if err != nil { + return err + } + expr := fmt.Sprintf(`(async () => { + const args = %s + try { + return await window.netlibTest[%q](...args) + } catch (e) { + const message = e && (e.stack || e.message) ? (e.stack || e.message) : String(e) + throw new Error(message) + } +})()`, argsJSON, name) + return p.run(defaultActionTimeout, chromedp.Evaluate(expr, result, evalAwaitPromise)) +} + +func (p *browserPlayer) run(timeout time.Duration, actions ...chromedp.Action) error { + ctx, cancel := context.WithTimeout(p.ctx, timeout) + defer cancel() + return chromedp.Run(ctx, actions...) +} + +func (p *browserPlayer) Close() error { + _ = p.call("closeNetwork", nil, "closing test suite") + p.cancel() + return nil +} + +func evalAwaitPromise(p *cdpRuntime.EvaluateParams) *cdpRuntime.EvaluateParams { + return p.WithAwaitPromise(true) +} + +type backendProcess struct { + name string + cmd *exec.Cmd + logMu sync.Mutex + logs bytes.Buffer + waitCh chan error +} + +func (w *scenarioWorld) startBackend(backend string) error { + if _, ok := w.backends[backend]; ok { + return nil + } + + port, err := getFreePort() + if err != nil { + return err + } + + var binary string + switch backend { + case "signaling": + binary = w.suite.signalingBinary + case "testproxy": + binary = w.suite.testproxyBinary + default: + return fmt.Errorf("unknown backend %q", backend) + } + + cmd := exec.Command(binary) + cmd.Env = append(os.Environ(), + "ADDR=127.0.0.1:"+port, + "ENV=test", + ) + if w.databaseURL != "" { + cmd.Env = append(cmd.Env, "DATABASE_URL="+w.databaseURL) + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + + proc := &backendProcess{ + name: backend, + cmd: cmd, + waitCh: make(chan error, 1), + } + + readyCh := make(chan error, 1) + if err := cmd.Start(); err != nil { + return err + } + + go copyBackendOutput(stdout, proc, nil) + go copyBackendOutput(stderr, proc, func(line string) { + entry := struct { + Severity string `json:"severity"` + Message string `json:"message"` + URL string `json:"url"` + }{} + if err := json.Unmarshal([]byte(line), &entry); err != nil { + return + } + severity := strings.ToLower(entry.Severity) + if entry.Message == "using database" && entry.URL != "" { + w.databaseURL = entry.URL + } + if entry.Message == "listening" { + select { + case readyCh <- nil: + default: + } + } + if severity == "error" || severity == "emergency" { + select { + case readyCh <- fmt.Errorf("error before backend was started: %s", entry.Message): + default: + } + } + }) + go func() { + proc.waitCh <- cmd.Wait() + close(proc.waitCh) + }() + + select { + case err := <-readyCh: + if err != nil { + _ = proc.Close() + return err + } + case err := <-proc.waitCh: + return fmt.Errorf("%s exited before it was ready: %w\n%s", backend, err, proc.Logs()) + case <-time.After(60 * time.Second): + _ = proc.Close() + return fmt.Errorf("%s did not start within 60s\n%s", backend, proc.Logs()) + } + + w.backends[backend] = proc + switch backend { + case "signaling": + w.signalingURL = "ws://127.0.0.1:" + port + "/v0/signaling" + case "testproxy": + w.testproxyURL = "http://127.0.0.1:" + port + } + return nil +} + +func copyBackendOutput(r io.Reader, proc *backendProcess, lineFunc func(string)) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + proc.appendLog(line + "\n") + if lineFunc != nil { + lineFunc(strings.TrimSpace(line)) + } + } + if err := scanner.Err(); err != nil { + proc.appendLog("error reading " + proc.name + " output: " + err.Error() + "\n") + } +} + +func getFreePort() (string, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return "", err + } + defer listener.Close() + _, port, err := net.SplitHostPort(listener.Addr().String()) + return port, err +} + +func (p *backendProcess) Close() error { + if p.cmd.Process == nil { + return nil + } + + _ = p.cmd.Process.Signal(syscall.SIGTERM) + select { + case <-p.waitCh: + return nil + case <-time.After(10 * time.Second): + _ = p.cmd.Process.Kill() + err := <-p.waitCh + if err != nil { + return fmt.Errorf("killed after timeout: %w", err) + } + return errors.New("killed after timeout") + } +} + +func (p *backendProcess) appendLog(line string) { + p.logMu.Lock() + defer p.logMu.Unlock() + _, _ = p.logs.WriteString(line) +} + +func (p *backendProcess) Logs() string { + p.logMu.Lock() + defer p.logMu.Unlock() + return p.logs.String() +} + +func withGeo(signalingURL string, country string, region string) (string, error) { + if country == "" && region == "" { + return signalingURL, nil + } + parsed, err := url.Parse(signalingURL) + if err != nil { + return "", err + } + q := parsed.Query() + if country != "" { + q.Set("country", country) + } + if region != "" { + q.Set("region", region) + } + parsed.RawQuery = q.Encode() + return parsed.String(), nil +} + +func parseJSON(raw string) (any, error) { + var value any + if err := json.Unmarshal([]byte(raw), &value); err != nil { + return nil, err + } + return value, nil +} + +func compactJSON(value any) string { + raw, err := json.Marshal(value) + if err != nil { + return fmt.Sprint(value) + } + return string(raw) +} + +func tableHashes(table *godog.Table) []map[string]string { + if table == nil || len(table.Rows) == 0 { + return nil + } + headers := make([]string, 0, len(table.Rows[0].Cells)) + for _, cell := range table.Rows[0].Cells { + headers = append(headers, cell.Value) + } + + hashes := make([]map[string]string, 0, len(table.Rows)-1) + for _, row := range table.Rows[1:] { + hash := make(map[string]string, len(headers)) + for i, header := range headers { + if i < len(row.Cells) { + hash[header] = row.Cells[i].Value + } else { + hash[header] = "" + } + } + hashes = append(hashes, hash) + } + return hashes +} + +func tableHeaders(table *godog.Table) []string { + if table == nil || len(table.Rows) == 0 { + return nil + } + headers := make([]string, 0, len(table.Rows[0].Cells)) + for _, cell := range table.Rows[0].Cells { + headers = append(headers, cell.Value) + } + return headers +} + +func rowsHash(table *godog.Table) map[string]string { + out := make(map[string]string) + if table == nil { + return out + } + for _, row := range table.Rows { + if len(row.Cells) >= 2 { + out[row.Cells[0].Value] = row.Cells[1].Value + } + } + return out +} + +func httpGetOK(rawURL string) error { + resp, err := http.Get(rawURL) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode/100 != 2 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("GET %s: %s: %s", rawURL, resp.Status, string(body)) + } + return nil +} + +func httpPostOK(rawURL string, body string) error { + resp, err := http.Post(rawURL, "text/plain", strings.NewReader(body)) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode/100 != 2 { + responseBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("POST %s: %s: %s", rawURL, resp.Status, string(responseBody)) + } + return nil +} + +func sqlQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", "''") + "'" +} diff --git a/features/browser_steps_test.go b/features/browser_steps_test.go new file mode 100644 index 0000000..d559fa5 --- /dev/null +++ b/features/browser_steps_test.go @@ -0,0 +1,834 @@ +package features + +import ( + "encoding/json" + "fmt" + "slices" + "strings" + "time" + + "github.com/cucumber/godog" +) + +func (w *scenarioWorld) registerSteps(ctx *godog.ScenarioContext) { + ctx.Step(`^the "([^"]*)" backend is running$`, w.backendIsRunning) + ctx.Step(`^webrtc is intercepted by the testproxy$`, w.webrtcIsInterceptedByTestproxy) + ctx.Step(`^webrtc is no longer intercepted by the testproxy$`, w.webrtcIsNoLongerInterceptedByTestproxy) + ctx.Step(`^the connection between "([^"]*)" and "([^"]*)" is interrupted until the first "([^"]*)" state$`, w.connectionBetweenPlayersIsInterruptedUntilState) + ctx.Step(`^the connection between "([^"]*)" and "([^"]*)" is interrupted$`, w.connectionBetweenPlayersIsInterrupted) + + ctx.Step(`^"([^"]*)" is connected as "([^"]*)" and ready for game "([^"]*)"$`, w.playerIsConnectedAndReadyForGame) + ctx.Step(`^"([^"]*)" is connected as "([^"]*)" with country,region as "([^"]*)","([^"]*)" and ready for game "([^"]*)"$`, w.playerIsConnectedWithGeoAndReadyForGame) + ctx.Step(`^"([^"]*)" creates a network for game "([^"]*)"$`, w.playerCreatesNetworkForGame) + ctx.Step(`^"([^"]*)" creates a lobby$`, w.playerCreatesLobby) + ctx.Step(`^"([^"]*)" creates a lobby with these settings:$`, w.playerCreatesLobbyWithSettings) + ctx.Step(`^"([^"]*)" connects to the lobby "([^"]*)"$`, w.playerConnectsToLobby) + ctx.Step(`^"([^"]*)" connects to the lobby "([^"]*)" with the password "([^"]*)"$`, w.playerConnectsToLobbyWithPassword) + ctx.Step(`^"([^"]*)" tries to connect to the lobby "([^"]*)" with the password "([^"]*)"$`, w.playerTriesToConnectToLobbyWithPassword) + ctx.Step(`^"([^"]*)" tries to connect to the lobby "([^"]*)" without a password$`, w.playerTriesToConnectToLobbyWithoutPassword) + ctx.Step(`^"([^"]*)" boardcasts "([^"]*)" over the reliable channel$`, w.playerBroadcastsOverReliableChannel) + ctx.Step(`^"([^"]*)" disconnects$`, w.playerDisconnects) + ctx.Step(`^"([^"]*)" leaves the lobby$`, w.playerLeavesLobby) + ctx.Step(`^"([^"]*)" requests lobbies with:$`, w.playerRequestsLobbiesWith) + ctx.Step(`^"([^"]*)" updates the lobby with these settings:$`, w.playerUpdatesLobbyWithSettings) + ctx.Step(`^"([^"]*)" fails to update the lobby with these settings:$`, w.playerFailsToUpdateLobbyWithSettings) + ctx.Step(`^"([^"]*)" disconnected from the signaling server$`, w.playerDisconnectedFromSignalingServer) + ctx.Step(`^the websocket of "([^"]*)" is reconnected$`, w.websocketOfPlayerIsReconnected) + + ctx.Step(`^"([^"]*)" receives the network event "([^"]*)"$`, w.playerReceivesNetworkEvent) + ctx.Step(`^"([^"]*)" receives the network event "([^"]*)" with the argument "([^"]*)"$`, w.playerReceivesNetworkEventWithArgument) + ctx.Step(`^"([^"]*)" receives the network event "([^"]*)" with the arguments "([^"]*)", "([^"]*)" and "([^"]*)"$`, w.playerReceivesNetworkEventWithThreeArguments) + ctx.Step(`^"([^"]*)" receives the network event "([^"]*)" with the arguments:$`, w.playerReceivesNetworkEventWithArguments) + ctx.Step(`^"([^"]*)" has recieved the peer ID "([^"]*)"$`, w.playerHasReceivedPeerID) + ctx.Step(`^"([^"]*)" should receive ([0-9]+) lobbies$`, w.playerShouldReceiveLobbyCount) + ctx.Step(`^"([^"]*)" should have received only these lobbies:$`, w.playerShouldHaveReceivedOnlyTheseLobbies) + ctx.Step(`^"([^"]*)" has not seen any "([^"]*)" event$`, w.playerHasNotSeenAnyEvent) + ctx.Step(`^"([^"]*)" has not seen a new "([^"]*)" event$`, w.playerHasNotSeenNewEvent) + ctx.Step(`^"([^"]*)" is the leader of the lobby$`, w.playerIsLeaderOfLobby) + ctx.Step(`^"([^"]*)" becomes the leader of the lobby$`, w.playerBecomesLeaderOfLobby) + ctx.Step(`^the latest error for "([^"]*)" is "([^"]*)"$`, w.latestErrorForPlayerIs) + ctx.Step(`^"([^"]*)" failed to join the lobby$`, w.playerFailedToJoinLobby) + + ctx.Step(`^"([^"]*)" are joined in a lobby$`, w.playersAreJoinedInLobby) + ctx.Step(`^"([^"]*)" are joined in a public lobby$`, w.playersAreJoinedInPublicLobby) + ctx.Step(`^"([^"]*)" are joined in a lobby for game "([^"]*)"$`, w.playersAreJoinedInLobbyForGame) + ctx.Step(`^these lobbies exist:$`, w.theseLobbiesExist) + ctx.Step(`^these peers exist:$`, w.thesePeersExist) +} + +func (w *scenarioWorld) backendIsRunning(backend string) error { + return w.startBackend(backend) +} + +func (w *scenarioWorld) webrtcIsInterceptedByTestproxy() error { + w.useTestProxy = true + return nil +} + +func (w *scenarioWorld) webrtcIsNoLongerInterceptedByTestproxy() error { + for _, player := range w.players { + if err := player.call("disableTestProxy", nil); err != nil { + return err + } + } + return nil +} + +func (w *scenarioWorld) connectionBetweenPlayersIsInterruptedUntilState(player0Name string, player1Name string, state string) error { + player0, err := w.player(player0Name) + if err != nil { + return err + } + player1ID, err := w.playerID(player1Name) + if err != nil { + return err + } + if w.testproxyURL == "" { + return fmt.Errorf("testproxy not active") + } + if err := player0.call("uninterruptOnConnectionState", nil, player1ID, state, w.testproxyURL); err != nil { + return err + } + return w.connectionBetweenPlayersIsInterrupted(player0Name, player1Name) +} + +func (w *scenarioWorld) connectionBetweenPlayersIsInterrupted(player0Name string, player1Name string) error { + player0ID, err := w.playerID(player0Name) + if err != nil { + return err + } + player1ID, err := w.playerID(player1Name) + if err != nil { + return err + } + if w.testproxyURL == "" { + return fmt.Errorf("testproxy not active") + } + if err := httpGetOK(fmt.Sprintf("%s/interrupt?id=%s", w.testproxyURL, player0ID+player1ID)); err != nil { + return err + } + return httpGetOK(fmt.Sprintf("%s/interrupt?id=%s", w.testproxyURL, player1ID+player0ID)) +} + +func (w *scenarioWorld) playerIsConnectedAndReadyForGame(playerName string, peerID string, gameID string) error { + return w.playerIsConnected(playerName, peerID, gameID, "", "") +} + +func (w *scenarioWorld) playerIsConnectedWithGeoAndReadyForGame(playerName string, peerID string, country string, region string, gameID string) error { + return w.playerIsConnected(playerName, peerID, gameID, country, region) +} + +func (w *scenarioWorld) playerIsConnected(playerName string, peerID string, gameID string, country string, region string) error { + player, err := w.createPlayer(playerName, gameID, country, region) + if err != nil { + return err + } + if err := w.waitForEvent(player, "ready", nil, true); err != nil { + return fmt.Errorf("unable to add player %s to network: %w", playerName, err) + } + actualID, err := w.playerID(playerName) + if err != nil { + return err + } + if actualID != peerID { + return fmt.Errorf("expected peer ID %s but got %s", peerID, actualID) + } + return nil +} + +func (w *scenarioWorld) playerCreatesNetworkForGame(playerName string, gameID string) error { + _, err := w.createPlayer(playerName, gameID, "", "") + return err +} + +func (w *scenarioWorld) createPlayer(playerName string, gameID string, country string, region string) (*browserPlayer, error) { + if w.signalingURL == "" { + return nil, fmt.Errorf("signaling backend is not running") + } + + signalingURL, err := withGeo(w.signalingURL, country, region) + if err != nil { + return nil, err + } + + player, err := w.suitePlayer(playerName) + if err != nil { + return nil, err + } + + options := map[string]any{ + "gameID": gameID, + "signalingURL": signalingURL, + } + if w.useTestProxy { + if w.testproxyURL == "" { + return nil, fmt.Errorf("testproxy not active") + } + options["testproxyURL"] = w.testproxyURL + } + if err := player.call("createPlayer", nil, options); err != nil { + _ = player.Close() + delete(w.players, playerName) + return nil, err + } + return player, nil +} + +func (w *scenarioWorld) suitePlayer(playerName string) (*browserPlayer, error) { + if player, ok := w.players[playerName]; ok { + return player, nil + } + player, err := w.newBrowserPlayer(playerName) + if err != nil { + return nil, err + } + w.players[playerName] = player + return player, nil +} + +func (w *scenarioWorld) playerCreatesLobby(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("createLobby", nil) +} + +func (w *scenarioWorld) playerCreatesLobbyWithSettings(playerName string) error { + settings, err := w.currentDocJSON() + if err != nil { + return err + } + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("createLobby", nil, settings) +} + +func (w *scenarioWorld) playerConnectsToLobby(playerName string, lobbyCode string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("joinLobby", nil, lobbyCode) +} + +func (w *scenarioWorld) playerConnectsToLobbyWithPassword(playerName string, lobbyCode string, password string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("joinLobby", nil, lobbyCode, password) +} + +func (w *scenarioWorld) playerTriesToConnectToLobbyWithPassword(playerName string, lobbyCode string, password string) error { + return w.playerTriesToConnectToLobby(playerName, lobbyCode, password) +} + +func (w *scenarioWorld) playerTriesToConnectToLobbyWithoutPassword(playerName string, lobbyCode string) error { + return w.playerTriesToConnectToLobby(playerName, lobbyCode, nil) +} + +func (w *scenarioWorld) playerTriesToConnectToLobby(playerName string, lobbyCode string, password any) error { + player, err := w.player(playerName) + if err != nil { + return err + } + var result struct { + OK bool `json:"ok"` + Message string `json:"message"` + } + if password == nil { + err = player.call("tryJoinLobby", &result, lobbyCode) + } else { + err = player.call("tryJoinLobby", &result, lobbyCode, password) + } + if err != nil { + return err + } + if !result.OK { + w.lastError[playerName] = result.Message + } + return nil +} + +func (w *scenarioWorld) playerBroadcastsOverReliableChannel(playerName string, message string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("broadcast", nil, "reliable", message) +} + +func (w *scenarioWorld) playerDisconnects(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("closeNetwork", nil) +} + +func (w *scenarioWorld) playerLeavesLobby(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("leaveLobby", nil) +} + +func (w *scenarioWorld) playerRequestsLobbiesWith(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + + if w.stepArg != nil && w.stepArg.DataTable != nil { + args := rowsHash(w.stepArg.DataTable) + var filter any + var sort any + var limit any + if raw := args["filter"]; raw != "" { + if filter, err = parseJSON(raw); err != nil { + return err + } + } + if raw := args["sort"]; raw != "" { + if sort, err = parseJSON(raw); err != nil { + return err + } + } + if raw := args["limit"]; raw != "" { + var parsed float64 + if err := json.Unmarshal([]byte(raw), &parsed); err != nil { + return err + } + limit = parsed + } + return player.call("listLobbies", nil, filter, sort, limit) + } + + filter, err := w.currentDocJSON() + if err != nil { + return err + } + return player.call("listLobbies", nil, filter) +} + +func (w *scenarioWorld) playerUpdatesLobbyWithSettings(playerName string) error { + settings, err := w.currentDocJSON() + if err != nil { + return err + } + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("setLobbySettings", nil, settings) +} + +func (w *scenarioWorld) playerFailsToUpdateLobbyWithSettings(playerName string) error { + settings, err := w.currentDocJSON() + if err != nil { + return err + } + player, err := w.player(playerName) + if err != nil { + return err + } + if err := player.call("setLobbySettings", nil, settings); err == nil { + return fmt.Errorf("no error thrown") + } + return nil +} + +func (w *scenarioWorld) playerDisconnectedFromSignalingServer(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("closeSignalingSocket", nil) +} + +func (w *scenarioWorld) websocketOfPlayerIsReconnected(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return player.call("forceReconnectSignaling", nil) +} + +func (w *scenarioWorld) playerReceivesNetworkEvent(playerName string, eventName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return w.waitForEvent(player, eventName, nil, true) +} + +func (w *scenarioWorld) playerReceivesNetworkEventWithArgument(playerName string, eventName string, expectedArgument string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return w.waitForEvent(player, eventName, []any{expectedArgument}, true) +} + +func (w *scenarioWorld) playerReceivesNetworkEventWithThreeArguments(playerName string, eventName string, arg0 string, arg1 string, arg2 string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + return w.waitForEvent(player, eventName, []any{arg0, arg1, arg2}, true) +} + +func (w *scenarioWorld) playerReceivesNetworkEventWithArguments(playerName string, eventName string) error { + args, err := w.currentDocJSON() + if err != nil { + return err + } + player, err := w.player(playerName) + if err != nil { + return err + } + return w.waitForEvent(player, eventName, args, true) +} + +func (w *scenarioWorld) playerHasReceivedPeerID(playerName string, expectedID string) error { + id, err := w.playerID(playerName) + if err != nil { + return err + } + if id == "" { + player, err := w.player(playerName) + if err != nil { + return err + } + if err := w.waitForEvent(player, "ready", nil, false); err != nil { + return err + } + id, err = w.playerID(playerName) + if err != nil { + return err + } + } + if id != expectedID { + return fmt.Errorf("expected peer ID %s but got %s", expectedID, id) + } + return nil +} + +func (w *scenarioWorld) playerShouldReceiveLobbyCount(playerName string, expectedLobbyCount int) error { + lobbies, err := w.playerLobbies(playerName) + if err != nil { + return err + } + if len(lobbies) != expectedLobbyCount { + return fmt.Errorf("expected %d lobbies but got %d", expectedLobbyCount, len(lobbies)) + } + return nil +} + +func (w *scenarioWorld) playerShouldHaveReceivedOnlyTheseLobbies(playerName string) error { + if w.stepArg == nil || w.stepArg.DataTable == nil { + return fmt.Errorf("expected lobby table") + } + lobbies, err := w.playerLobbies(playerName) + if err != nil { + return err + } + expectedRows := tableHashes(w.stepArg.DataTable) + headers := tableHeaders(w.stepArg.DataTable) + + for _, row := range expectedRows { + code := row["code"] + var matches []map[string]any + for _, lobby := range lobbies { + if fmt.Sprint(lobby["code"]) == code { + matches = append(matches, lobby) + } + } + if len(matches) != 1 { + return fmt.Errorf("expected to find one lobby with code %s but found %d in [%s]", code, len(matches), lobbyCodes(lobbies)) + } + lobby := matches[0] + for _, header := range headers { + want := row[header] + got := lobby[header] + if gotMap, ok := got.(map[string]any); ok { + if compactJSON(gotMap) != compactExpectedJSON(want) { + return fmt.Errorf("expected %s to be %s but got %s", header, want, compactJSON(gotMap)) + } + continue + } + if fmt.Sprint(got) != want { + return fmt.Errorf("expected %s to be %s but got %v", header, want, got) + } + } + } + if len(lobbies) != len(expectedRows) { + return fmt.Errorf("expected %d lobbies but got %d", len(expectedRows), len(lobbies)) + } + return nil +} + +func compactExpectedJSON(raw string) string { + var parsed any + if err := json.Unmarshal([]byte(raw), &parsed); err != nil { + return raw + } + return compactJSON(parsed) +} + +func lobbyCodes(lobbies []map[string]any) string { + codes := make([]string, 0, len(lobbies)) + for _, lobby := range lobbies { + codes = append(codes, fmt.Sprint(lobby["code"])) + } + return strings.Join(codes, ", ") +} + +func (w *scenarioWorld) playerHasNotSeenAnyEvent(playerName string, eventName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + var event any + if err := player.call("findEvent", &event, eventName, []any{}); err != nil { + return err + } + if event != nil { + return fmt.Errorf("%s has recieved a %s event: %s", playerName, eventName, compactJSON(event)) + } + return nil +} + +func (w *scenarioWorld) playerHasNotSeenNewEvent(playerName string, eventName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + var event any + if err := player.call("findNewEvent", &event, eventName, []any{}); err != nil { + return err + } + if event != nil { + return fmt.Errorf("%s has recieved a %s event: %s", playerName, eventName, compactJSON(event)) + } + return nil +} + +func (w *scenarioWorld) playerIsLeaderOfLobby(playerName string) error { + id, err := w.playerID(playerName) + if err != nil { + return err + } + leader, err := w.playerCurrentLeader(playerName) + if err != nil { + return err + } + if leader != id { + return fmt.Errorf("player is not the leader") + } + return nil +} + +func (w *scenarioWorld) playerBecomesLeaderOfLobby(playerName string) error { + id, err := w.playerID(playerName) + if err != nil { + return err + } + player, err := w.player(playerName) + if err != nil { + return err + } + if err := w.waitForEvent(player, "leader", []any{id}, false); err != nil { + return fmt.Errorf("no event leader(%s) received: %w", id, err) + } + leader, err := w.playerCurrentLeader(playerName) + if err != nil { + return err + } + if leader != id { + return fmt.Errorf("player is not the leader") + } + return nil +} + +func (w *scenarioWorld) latestErrorForPlayerIs(playerName string, message string) error { + if got := w.lastError[playerName]; got != message { + return fmt.Errorf("expected error to be %q but got %q", message, got) + } + return nil +} + +func (w *scenarioWorld) playerFailedToJoinLobby(playerName string) error { + player, err := w.player(playerName) + if err != nil { + return err + } + var lobby string + if err := player.call("getCurrentLobby", &lobby); err != nil { + return err + } + if lobby != "" { + return fmt.Errorf("player is in lobby %s", lobby) + } + return nil +} + +func (w *scenarioWorld) playersAreJoinedInLobby(playerNamesRaw string) error { + return w.playersAreJoinedInALobby(playerNamesRaw, false) +} + +func (w *scenarioWorld) playersAreJoinedInPublicLobby(playerNamesRaw string) error { + return w.playersAreJoinedInALobby(playerNamesRaw, true) +} + +func (w *scenarioWorld) playersAreJoinedInLobbyForGame(playerNamesRaw string, gameID string) error { + playerNames := splitPlayerNames(playerNamesRaw) + if len(playerNames) < 2 { + return fmt.Errorf("need at least 2 players to join a lobby") + } + for _, playerName := range playerNames { + player, err := w.createPlayer(playerName, gameID, "", "") + if err != nil { + return err + } + if err := w.waitForEvent(player, "ready", nil, true); err != nil { + return fmt.Errorf("unable to add player %s to network: %w", playerName, err) + } + } + return w.playersAreJoinedInALobby(playerNamesRaw, false) +} + +func (w *scenarioWorld) playersAreJoinedInALobby(playerNamesRaw string, public bool) error { + playerNames := splitPlayerNames(playerNamesRaw) + if len(playerNames) < 2 { + return fmt.Errorf("need at least 2 players to join a lobby") + } + first, err := w.player(playerNames[0]) + if err != nil { + return err + } + + settings := map[string]any{"public": public} + if err := first.call("createLobby", nil, settings); err != nil { + return err + } + event, err := w.eventFor(first, "lobby", nil, true) + if err != nil { + return err + } + lobbyCode, _ := event.PayloadString(0) + + for _, playerName := range playerNames[1:] { + player, err := w.player(playerName) + if err != nil { + return err + } + if err := player.call("joinLobby", nil, lobbyCode); err != nil { + return err + } + if err := w.waitForEvent(player, "lobby", nil, true); err != nil { + return err + } + } + + for _, playerName := range playerNames { + player, err := w.player(playerName) + if err != nil { + return err + } + for i := 0; i < len(playerNames)-1; i++ { + if err := w.waitForEvent(player, "connected", nil, true); err != nil { + return err + } + } + count, err := w.playerPeerCount(playerName) + if err != nil { + return err + } + if count != len(playerNames)-1 { + return fmt.Errorf("player not connected with enough others") + } + } + return nil +} + +func (w *scenarioWorld) theseLobbiesExist() error { + if w.stepArg == nil || w.stepArg.DataTable == nil { + return fmt.Errorf("expected lobbies table") + } + if w.testproxyURL == "" { + return fmt.Errorf("testproxy not active") + } + + headers := tableHeaders(w.stepArg.DataTable) + rows := tableHashes(w.stepArg.DataTable) + columns := make([]string, 0, len(headers)) + values := make([]string, 0, len(rows)) + + for _, row := range rows { + rowValues := make([]string, 0, len(headers)) + for _, key := range headers { + value := row[key] + column := key + sqlValue := "" + if key == "playerCount" { + column = "peers" + n := 0 + _, _ = fmt.Sscanf(value, "%d", &n) + peers := make([]string, 0, n) + for i := 0; i < n; i++ { + peers = append(peers, sqlQuote(fmt.Sprintf("peer%d", i))) + } + sqlValue = fmt.Sprintf("ARRAY[%s]::VARCHAR(20)[]", strings.Join(peers, ", ")) + } else if value == "null" { + sqlValue = "NULL" + } else { + sqlValue = sqlQuote(value) + } + if !contains(columns, column) { + columns = append(columns, column) + } + rowValues = append(rowValues, sqlValue) + } + values = append(values, fmt.Sprintf("(%s)", strings.Join(rowValues, ", "))) + } + + sql := "INSERT INTO lobbies (" + strings.Join(columns, ", ") + ") VALUES " + strings.Join(values, ", ") + return httpPostOK(w.testproxyURL+"/sql", sql) +} + +func (w *scenarioWorld) thesePeersExist() error { + if w.stepArg == nil || w.stepArg.DataTable == nil { + return fmt.Errorf("expected peers table") + } + if w.testproxyURL == "" { + return fmt.Errorf("testproxy not active") + } + + headers := tableHeaders(w.stepArg.DataTable) + rows := tableHashes(w.stepArg.DataTable) + values := make([]string, 0, len(rows)) + for _, row := range rows { + rowValues := make([]string, 0, len(headers)) + for _, key := range headers { + value := row[key] + if value == "null" { + rowValues = append(rowValues, "NULL") + } else { + rowValues = append(rowValues, sqlQuote(value)) + } + } + values = append(values, fmt.Sprintf("(%s)", strings.Join(rowValues, ", "))) + } + sql := "INSERT INTO peers (" + strings.Join(headers, ", ") + ") VALUES " + strings.Join(values, ", ") + return httpPostOK(w.testproxyURL+"/sql", sql) +} + +func (w *scenarioWorld) player(playerName string) (*browserPlayer, error) { + player, ok := w.players[playerName] + if !ok { + return nil, fmt.Errorf("no such player: %s", playerName) + } + return player, nil +} + +func (w *scenarioWorld) playerID(playerName string) (string, error) { + player, err := w.player(playerName) + if err != nil { + return "", err + } + var id string + if err := player.call("getID", &id); err != nil { + return "", err + } + return id, nil +} + +func (w *scenarioWorld) playerPeerCount(playerName string) (int, error) { + player, err := w.player(playerName) + if err != nil { + return 0, err + } + var count int + if err := player.call("getPeerCount", &count); err != nil { + return 0, err + } + return count, nil +} + +func (w *scenarioWorld) playerCurrentLeader(playerName string) (string, error) { + player, err := w.player(playerName) + if err != nil { + return "", err + } + var leader string + if err := player.call("getCurrentLeader", &leader); err != nil { + return "", err + } + return leader, nil +} + +func (w *scenarioWorld) playerLobbies(playerName string) ([]map[string]any, error) { + player, err := w.player(playerName) + if err != nil { + return nil, err + } + var lobbies []map[string]any + if err := player.call("getLastReceivedLobbies", &lobbies); err != nil { + return nil, err + } + return lobbies, nil +} + +func (w *scenarioWorld) waitForEvent(player *browserPlayer, eventName string, matchArguments any, consume bool) error { + _, err := w.eventFor(player, eventName, matchArguments, consume) + return err +} + +func (w *scenarioWorld) eventFor(player *browserPlayer, eventName string, matchArguments any, consume bool) (recordedEvent, error) { + if matchArguments == nil { + matchArguments = []any{} + } + var event recordedEvent + err := player.call("waitForEvent", &event, eventName, matchArguments, consume, int(waitEventTimeout/time.Millisecond)) + return event, err +} + +type recordedEvent struct { + EventName string `json:"eventName"` + Payload []any `json:"eventPayload"` +} + +func (e recordedEvent) PayloadString(index int) (string, bool) { + if index >= len(e.Payload) { + return "", false + } + return fmt.Sprint(e.Payload[index]), true +} + +func (w *scenarioWorld) currentDocJSON() (any, error) { + if w.stepArg == nil || w.stepArg.DocString == nil { + return nil, fmt.Errorf("expected doc string") + } + return parseJSON(w.stepArg.DocString.Content) +} + +func splitPlayerNames(raw string) []string { + parts := strings.Split(raw, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out +} + +func contains(values []string, needle string) bool { + return slices.Contains(values, needle) +} diff --git a/features/support/steps/backend.ts b/features/support/steps/backend.ts deleted file mode 100644 index dc167ec..0000000 --- a/features/support/steps/backend.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { spawn } from 'child_process' -import { After, Given } from '@cucumber/cucumber' -import { World } from '../world' - -Given('the {string} backend is running', async function (this: World, backend: string) { - return await new Promise((resolve, reject) => { - const port = 10000 + Math.ceil(Math.random() * 1000) - const env: NodeJS.ProcessEnv = { - ...process.env, - ADDR: `127.0.0.1:${port}`, - ENV: 'test' - } - - if (this.databaseURL !== undefined) { - env.DATABASE_URL = this.databaseURL - } - - const prc = spawn(`/tmp/netlib-cucumber-${backend}`, [], { - windowsHide: true, - env - }) - prc.stderr.setEncoding('utf8') - - let resolved = false - prc.stderr.on('data', (data: string) => { - const lines = data.split('\n') - lines.forEach(line => { - if (line === '') { - return - } - try { - const entry = JSON.parse(line) - const severity = entry.severity.toLowerCase() - if (!resolved && (severity === 'error' || severity === 'emergency')) { - return reject(new Error(`error before backend was started: ${entry.message as string}`)) - } - if (entry.message === 'using database') { - this.databaseURL = entry.url - } else if (entry.message === 'listening') { - resolved = true - resolve(undefined) - } - } catch (_) { - } - this.print(line) - }) - }) - prc.addListener('exit', () => { - this.print(`${backend} exited`) - - if (!resolved) { - reject(new Error(`${backend} exited before it was ready`)) - } - }) - - // Create a promise that resolves when the backend is closed so - // we can block for it in the After step that kills the backend: - const waiter = new Promise(resolve => { - prc.addListener('close', () => { - this.print(`${backend} closed (exitcode: ${prc.exitCode ?? 0})`) - prc.unref() - prc.removeAllListeners() - resolve() - }) - }) - - this.backends.set(backend, { process: prc, port, wait: waiter }) - switch (backend) { - case 'signaling': - this.signalingURL = `ws://127.0.0.1:${port}/v0/signaling` - break - case 'testproxy': - this.testproxyURL = `http://127.0.0.1:${port}` - break - } - }) -}) - -After(async function (this: World) { - for (const [key, backend] of this.backends) { - this.print('killing ' + key) - backend.process.kill() - await backend.wait // wait for the backend to close - } -}) diff --git a/features/support/steps/network.ts b/features/support/steps/network.ts deleted file mode 100644 index dabcc99..0000000 --- a/features/support/steps/network.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { After, DataTable, Given, Then, When } from '@cucumber/cucumber' -import { World } from '../world' - -After(async function (this: World) { - this.players.forEach(p => { - p.network.close('closing test suite') - }) - this.players.clear() -}) - -async function playerIsConnectedAndReadyForGame (this: World, playerName: string, peerID: string, gameID: string, country?: string, region?: string): Promise { - const player = await this.createPlayer(playerName, gameID, country, region) - const event = await player.waitForEvent('ready') - if (event == null) { - throw new Error(`unable to add player ${playerName} to network`) - } - if (player.network.id !== peerID) { - throw new Error(`expected peer ID ${peerID} but got ${player.network.id}`) - } -} - -Given('{string} is connected as {string} and ready for game {string}', async function (this: World, playerName: string, peerID: string, gameID: string) { - await playerIsConnectedAndReadyForGame.call(this, playerName, peerID, gameID) -}) - -Given('{string} is connected as {string} with country,region as {string},{string} and ready for game {string}', async function (this: World, playerName: string, peerID: string, country: string, region: string, gameID: string) { - await playerIsConnectedAndReadyForGame.call(this, playerName, peerID, gameID, country, region) -}) - -async function areJoinedInALobby (this: World, playerNamesRaw: string, publc: boolean): Promise { - const playerNames = playerNamesRaw.split(',').map(s => s.trim()) - if (playerNames.length < 2) { - throw new Error('need at least 2 players to join a lobby') - } - const first = this.players.get(playerNames[0]) - if (first === undefined) { - throw new Error(`player ${playerNames[0]} not found`) - } - - void first.network.create({ - public: publc - }) - const lobbyEvent = await first.waitForEvent('lobby') - const lobbyCode = lobbyEvent.eventPayload[0] as string - - for (let i = 1; i < playerNames.length; i++) { - const playerName = playerNames[i] - const player = this.players.get(playerName) - if (player == null) { - throw new Error(`player ${playerName} not found`) - } - void player.network.join(lobbyCode) - await player.waitForEvent('lobby') - } - - for (let i = 0; i < playerNames.length; i++) { - const playerName = playerNames[i] - const player = this.players.get(playerName) - for (let j = 0; j < playerNames.length - 1; j++) { - await player?.waitForEvent('connected') - } - if (player?.network.peers.size !== playerNames.length - 1) { - throw new Error('player not connected with enough others') - } - } -} - -Given('{string} are joined in a lobby', async function (this: World, playerNamesRaw: string) { - await areJoinedInALobby.call(this, playerNamesRaw, false) -}) - -Given('{string} are joined in a public lobby', async function (this: World, playerNamesRaw: string) { - await areJoinedInALobby.call(this, playerNamesRaw, true) -}) - -Given('{string} are joined in a lobby for game {string}', async function (this: World, playerNamesRaw: string, gameID: string) { - const playerNames = playerNamesRaw.split(',').map(s => s.trim()) - if (playerNames.length < 2) { - throw new Error('need at least 2 players to join a lobby') - } - - for (let i = 0; i < playerNames.length; i++) { - const playerName = playerNames[i] - const player = await this.createPlayer(playerName, gameID) - const event = await player.waitForEvent('ready') - if (event == null) { - throw new Error(`unable to add player ${playerName} to network`) - } - } - - await areJoinedInALobby.call(this, playerNamesRaw, false) -}) - -Given('these lobbies exist:', async function (this: World, lobbies: DataTable) { - if (this.testproxyURL === undefined) { - throw new Error('testproxy not active') - } - - const columns: string[] = [] - const values: string[] = [] - - lobbies.hashes().forEach(row => { - const v: string[] = [] - - Object.keys(row).forEach(key => { - const value = row[key] - if (key === 'playerCount') { - if (!columns.includes('peers')) { - columns.push('peers') - } - - const n = parseInt(value, 10) - const peers: string[] = [] - - for (let i = 0; i < n; i++) { - peers.push(`'peer${i}'`) - } - - v.push(`ARRAY[${peers.join(', ')}]::VARCHAR(20)[]`) - } else { - if (!columns.includes(key)) { - columns.push(key) - } - - if (value === 'null') { - v.push('NULL') - } else { - v.push(`'${value}'`) - } - } - }) - - values.push(`(${v.join(', ')})`) - }) - - await fetch(`${this.testproxyURL}/sql`, { - method: 'POST', - body: 'INSERT INTO lobbies (' + columns.join(', ') + ') VALUES ' + values.join(', ') - }) -}) - -Given('these peers exist:', async function (this: World, peers: DataTable) { - if (this.testproxyURL === undefined) { - throw new Error('testproxy not active') - } - - const columns: string[] = [] - const values: string[] = [] - - peers.hashes().forEach(row => { - const v: string[] = [] - - Object.keys(row).forEach(key => { - const value = row[key] - if (!columns.includes(key)) { - columns.push(key) - } - - if (value === 'null') { - v.push('NULL') - } else { - v.push(`'${value}'`) - } - }) - - values.push(`(${v.join(', ')})`) - }) - - await fetch(`${this.testproxyURL}/sql`, { - method: 'POST', - body: 'INSERT INTO peers (' + columns.join(', ') + ') VALUES ' + values.join(', ') - }) -}) - -When('{string} creates a network for game {string}', async function (this: World, playerName: string, gameID: string) { - await this.createPlayer(playerName, gameID) -}) - -When('{string} creates a lobby', async function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - await player.network.create() -}) - -When('{string} creates a lobby with these settings:', async function (this: World, playerName: string, settingsBlob: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const settings = JSON.parse(settingsBlob) - await player.network.create(settings) -}) - -When('{string} connects to the lobby {string}', async function (this: World, playerName: string, lobbyCode: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - await player.network.join(lobbyCode) -}) - -When('{string} connects to the lobby {string} with the password {string}', async function (this: World, playerName: string, lobbyCode: string, password: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - await player.network.join(lobbyCode, password) -}) - -When('{string} tries to connect to the lobby {string} with the password {string}', async function (this: World, playerName: string, lobbyCode: string, password: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - try { - this.lastError.delete(playerName) - await player.network.join(lobbyCode, password) - } catch (e) { - this.lastError.set(playerName, e as Error) - } -}) - -When('{string} tries to connect to the lobby {string} without a password', async function (this: World, playerName: string, lobbyCode: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - try { - this.lastError.delete(playerName) - await player.network.join(lobbyCode) - } catch (e) { - this.lastError.set(playerName, e as Error) - } -}) - -When('{string} boardcasts {string} over the reliable channel', function (this: World, playerName: string, message: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - player.network.broadcast('reliable', message) -}) - -When('{string} disconnects', async function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - player.network.close() -}) - -When('{string} leaves the lobby', async function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - await player.network.leave() -}) - -When('{string} requests lobbies with:', async function (this: World, playerName: string, payload: string | DataTable) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - let filter: any - let sort: Record | undefined - let limit: number | undefined - - if (typeof payload !== 'string') { - const argsHash = payload.rowsHash() - filter = argsHash.filter != null ? JSON.parse(argsHash.filter) : undefined - sort = argsHash.sort != null ? JSON.parse(argsHash.sort) : undefined - limit = argsHash.limit != null ? parseInt(argsHash.limit, 10) : undefined - } else { - filter = JSON.parse(payload) - } - - const lobbies = await player.network.list(filter, sort, limit) - player.lastReceivedLobbies = lobbies -}) - -Then('{string} receives the network event {string}', async function (this: World, playerName: string, eventName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = await player.waitForEvent(eventName) - if (event == null) { - throw new Error(`no event ${eventName} received`) - } -}) - -Then('{string} receives the network event {string} with the argument {string}', async function (this: World, playerName: string, eventName: string, expectedArgument: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = await player.waitForEvent(eventName, [expectedArgument]) - if (event == null) { - throw new Error(`no event ${eventName}(${expectedArgument}) received`) - } -}) - -Then('{string} receives the network event {string} with the arguments {string}, {string} and {string}', async function (this: World, playerName: string, eventName: string, expectedArgument0: string, expectedArgument1: string, expectedArgument2: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = await player.waitForEvent(eventName, [expectedArgument0, expectedArgument1, expectedArgument2]) - if (event == null) { - throw new Error(`no event ${eventName}(${expectedArgument0}, ${expectedArgument1}, ${expectedArgument2}) received`) - } -}) - -Then('{string} receives the network event {string} with the arguments:', async function (this: World, playerName: string, eventName: string, args: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = await player.waitForEvent(eventName, JSON.parse(args)) - if (event == null) { - throw new Error(`no event ${eventName}(...) received`) - } -}) - -Then('{string} has recieved the peer ID {string}', async function (this: World, playerName: string, exepctedID: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - if (player.network.id === '') { - await player.waitForEvent('ready', [], false) - } - if (player.network.id !== exepctedID) { - throw new Error(`expected peer ID ${exepctedID} but got ${player.network.id}`) - } -}) - -Then('{string} should receive {int} lobbies', function (this: World, playerName: string, expectedLobbyCount: number) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - if (player.lastReceivedLobbies?.length !== expectedLobbyCount) { - throw new Error(`expected ${expectedLobbyCount} lobbies but got ${player.lastReceivedLobbies?.length}`) - } -}) - -Then('{string} should have received only these lobbies:', function (this: World, playerName: string, expectedLobbies: DataTable) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - expectedLobbies.hashes().forEach(row => { - const correctCodeLobby = player.lastReceivedLobbies.filter(lobby => lobby.code === row.code) - if (correctCodeLobby.length !== 1) { - throw new Error(`expected to find one lobby with code ${row.code} but found ${correctCodeLobby.length} in [${player.lastReceivedLobbies.map(l => l.code).join(', ')}]`) - } - const lobby = correctCodeLobby[0] as any - Object.keys(lobby).forEach(key => { - if (!Object.prototype.hasOwnProperty.call(row, key)) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete lobby[key] - } - }) - const want = row as any - Object.keys(row).forEach(key => { - if (typeof lobby[key] === 'object') { - if (JSON.stringify(lobby[key]) !== `${want[key] as string}`) { - throw new Error(`expected ${key} to be ${want[key] as string} but got ${JSON.stringify(lobby[key])}`) - } - } else { - if (`${lobby[key] as string}` !== `${want[key] as string}`) { - throw new Error(`expected ${key} to be ${want[key] as string} but got ${lobby[key] as string}`) - } - } - }) - }) - if (player.lastReceivedLobbies.length !== expectedLobbies.hashes().length) { - throw new Error(`expected ${expectedLobbies.hashes().length} lobbies but got ${player.lastReceivedLobbies.length}`) - } -}) - -// "has not seen any" checks all events ever received. -When('{string} has not seen any {string} event', function (this: World, playerName: string, eventName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = player.findEvent(eventName) - if (event !== undefined) { - throw new Error(`${playerName} has recieved a ${eventName} event: ${event.eventPayload[0] as string}`) - } -}) - -// "has not seen a new" only checks new events since the last "receives the network event". -When('{string} has not seen a new {string} event', function (this: World, playerName: string, eventName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = player.findNewEvent(eventName) - if (event !== undefined) { - throw new Error(`${playerName} has recieved a ${eventName} event: ${event.eventPayload[0] as string}`) - } -}) - -When('{string} disconnected from the signaling server', function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - ;(player.network as any).signaling.ws.close() -}) - -When('the websocket of {string} is reconnected', function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - player.network._forceReconnectSignaling() -}) - -Given('{string} is the leader of the lobby', async function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - if (player.network.currentLeader !== player.network.id) { - throw new Error('player is not the leader') - } -}) - -Given('{string} becomes the leader of the lobby', async function (this: World, playerName: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const event = await player.waitForEvent('leader', [player.network.id], false) - if (event == null) { - throw new Error(`no event leader(${player.network.id}) received`) - } - if (player.network.currentLeader !== player.network.id) { - throw new Error('player is not the leader') - } -}) - -When('{string} updates the lobby with these settings:', async function (this: World, playerName: string, settings: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - const r = await player.network.setLobbySettings(JSON.parse(settings)) - if (r !== true) { - throw new Error(`failed to update lobby: ${r.message}`) - } -}) - -When('{string} fails to update the lobby with these settings:', async function (this: World, playerName: string, settings: string) { - const player = this.players.get(playerName) - if (player == null) { - throw new Error('no such player') - } - try { - await player.network.setLobbySettings(JSON.parse(settings)) - } catch (e) { - return // we expect this to fail - } - throw new Error('no error thrown') -}) - -Then('the latest error for {string} is {string}', function (playerName: string, message: string) { - const error = this.lastError.get(playerName) - if (error === undefined) { - throw new Error('no error thrown') - } else if (error.message !== message) { - throw new Error(`expected error to be '${message}' but got '${error.message as string}'`) - } -}) - -Then('{string} failed to join the lobby', function (playerName: string) { - const player = this.players.get(playerName) - if (player === null) { - throw new Error('no such player') - } - - if (player.network.currentLobby !== undefined) { - throw new Error(`player is in lobby ${player.network.currentLobby as string}`) - } -}) diff --git a/features/support/steps/proxy.ts b/features/support/steps/proxy.ts deleted file mode 100644 index fd5976d..0000000 --- a/features/support/steps/proxy.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Given, When } from '@cucumber/cucumber' -import { World } from '../world' - -Given('webrtc is intercepted by the testproxy', function (this: World) { - this.useTestProxy = true -}) - -When('webrtc is no longer intercepted by the testproxy', function (this: World) { - this.players.forEach(player => { - player.network.peers.forEach(peer => { - peer.config.testproxyURL = undefined - }) - }) -}) - -When('the connection between {string} and {string} is interrupted until the first {string} state', async function (this: World, player0Name: string, player1Name: string, state: string) { - const player0 = this.players.get(player0Name) - if (player0 == null) { - throw new Error('no such player: ' + player0Name) - } - const player1 = this.players.get(player1Name) - if (player1 == null) { - throw new Error('no such player: ' + player1Name) - } - if (this.testproxyURL === undefined) { - throw new Error('testproxy not active') - } - - await fetch(`${this.testproxyURL}/interrupt?id=${player0.network.id + player1.network.id}`) - await fetch(`${this.testproxyURL}/interrupt?id=${player1.network.id + player0.network.id}`) - - const connToPlayer1 = player0.network.peers.get(player1.network.id)?.conn - if (connToPlayer1 !== undefined) { - connToPlayer1.addEventListener('connectionstatechange', () => { - if (connToPlayer1?.connectionState === state) { - fetch(`${this.testproxyURL ?? ''}/uninterrupt?id=${player0.network.id + player1.network.id}`).then(() => {}).catch(console.error) - fetch(`${this.testproxyURL ?? ''}/uninterrupt?id=${player1.network.id + player0.network.id}`).then(() => {}).catch(console.error) - } - }) - } -}) - -When('the connection between {string} and {string} is interrupted', async function (this: World, player0Name: string, player1Name: string) { - const player0 = this.players.get(player0Name) - if (player0 == null) { - throw new Error('no such player: ' + player0Name) - } - const player1 = this.players.get(player1Name) - if (player1 == null) { - throw new Error('no such player: ' + player1Name) - } - if (this.testproxyURL === undefined) { - throw new Error('testproxy not active') - } - - await fetch(`${this.testproxyURL}/interrupt?id=${player0.network.id + player1.network.id}`) - await fetch(`${this.testproxyURL}/interrupt?id=${player1.network.id + player0.network.id}`) -}) diff --git a/features/support/types.ts b/features/support/types.ts deleted file mode 100644 index 0924712..0000000 --- a/features/support/types.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Network } from '../../lib' -import { LobbyListEntry } from '../../lib/types' - -interface RecordedEvent { - eventName: string - eventPayload: IArguments -} - -const allEvents = ['close', 'ready', 'lobby', 'left', 'connected', 'disconnected', 'reconnecting', 'reconnected', 'message', 'signalingerror', 'signalingreconnected', 'leader', 'lobbyUpdated'] - -export class Player { - public lastReceivedLobbies: LobbyListEntry[] = [] - public events: RecordedEvent[] = [] - public scanIndex = 0 - - constructor (public name: string, public network: Network) { - allEvents.forEach(eventName => { - const events = this.events - this.network.on(eventName as any, function () { - // console.log(name, `(${network.id})`, 'received event', eventName, `${arguments[0] as string}`, arguments[1], arguments[2]) - events.push({ - eventName, - eventPayload: arguments - }) - }) - }) - - network.on('signalingerror', _ => {}) - network.on('rtcerror', _ => {}) - } - - // findEvent finds the first event matching the eventName and matchArguments. - findEvent (eventName: string, matchArguments: any[] = []): RecordedEvent | undefined { - return this.events.find(e => matchEvent(e, eventName, matchArguments)) - } - - // findRecentEvent finds events starting after the last consumed event. - findNewEvent (eventName: string, matchArguments: any[] = []): RecordedEvent | undefined { - return this.events.slice(this.scanIndex).find(e => matchEvent(e, eventName, matchArguments)) - } - - // waitForEvent waits for an event to be received, matching the eventName and matchArguments. - // If consume is true, the event will be consumed and the scanIndex will be incremented. - async waitForEvent (eventName: string, matchArguments: any[] = [], consume: boolean = true): Promise { - if (!allEvents.includes(eventName)) { - throw new Error(`Event type ${eventName} not tracked, add to allEvents in types.ts`) - } - - const find = (): RecordedEvent | null => { - const ix = this.events.slice(this.scanIndex).findIndex(e => matchEvent(e, eventName, matchArguments)) - if (ix >= 0) { - const event = this.events[this.scanIndex + ix] - if (consume) { - this.scanIndex += ix + 1 - } - return event - } - return null - } - - return await new Promise((resolve, reject) => { - const event = find() - if (event !== null) { - resolve(event) - } else { - let interval: NodeJS.Timeout | null = null - const timeout = setTimeout(() => { - const sameEvents = this.events.slice(this.scanIndex).filter(e => e.eventName === eventName) - const others = sameEvents.map(e => Array.from(e.eventPayload).map(a => `${JSON.stringify(a)}`).join(',')).join(' + ') - if (interval !== null) { - clearInterval(interval) - } - reject(new Error(`Event not found, timed out, got: ${others}`)) - }, 20000) - interval = setInterval(() => { - const event = find() - if (event !== null) { - if (interval !== null) { - clearInterval(interval) - } - clearTimeout(timeout) - resolve(event) - } - }, 100) - } - }) - } -} - -function matchEvent (e: RecordedEvent, eventName: string, matchArguments: any[] = []): boolean { - if (e.eventName !== eventName) { - return false - } - let argumentsMatch = true - matchArguments.forEach((arg, i) => { - if (typeof arg === 'string' || arg instanceof String) { - // Fool typescript into calling toString() on the event argument: - argumentsMatch = `${e.eventPayload[i] as string}` === arg - } else if (e.eventPayload[i] instanceof Object) { - const a = { ...e.eventPayload[i] } - delete a.createdAt - delete a.updatedAt - delete arg.createdAt - delete arg.updatedAt - argumentsMatch = JSON.stringify(a) === JSON.stringify(arg) - } else { - argumentsMatch = e.eventPayload[i] === arg - } - return argumentsMatch - }) - return argumentsMatch -} diff --git a/features/support/world.ts b/features/support/world.ts deleted file mode 100644 index a372d31..0000000 --- a/features/support/world.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { After, AfterAll, Before, BeforeAll, setWorldConstructor, World as CucumberWorld, setDefaultTimeout } from '@cucumber/cucumber' -import { spawn } from 'child_process' -import { unlinkSync } from 'fs' - -import fetch from 'node-fetch' -import ws from 'ws' -import wrtc from '@roamhq/wrtc' - -import { Player } from './types' -import { PeerConfiguration } from '../../lib/types' - -import { Network } from '../../lib' - -;(global as any).fetch = fetch -;(global as any).WebSocket = ws -;(global as any).RTCPeerConnection = wrtc.RTCPeerConnection - -process.env.NODE_ENV = 'test' - -let anyScenarioFailed = false - -interface backend { - process: ReturnType - port: number - wait: Promise -} - -setDefaultTimeout(120 * 1000) - -export class World extends CucumberWorld { - public scenarioRunning: boolean = false - - public backends: Map = new Map() - - public signalingURL?: string - public testproxyURL?: string - public useTestProxy: boolean = false - public databaseURL?: string - - public players: Map = new Map() - public lastError: Map = new Map() - - public print (message: string): void { - if (this.scenarioRunning) { - void this.attach(message) - } else { - // void this.attach(message) - } - } - - public async createPlayer (playerName: string, gameID: string, country?: string, region?: string): Promise { - return await new Promise((resolve) => { - const config: PeerConfiguration = {} - if (this.useTestProxy) { - config.testproxyURL = this.testproxyURL - } - let signalingURL = this.signalingURL - if (signalingURL !== undefined && (country !== undefined || region !== undefined)) { - const url = new URL(signalingURL) - if (country !== undefined) { - url.searchParams.set('country', country) - } - if (region !== undefined) { - url.searchParams.set('region', region) - } - signalingURL = url.toString() - } - - const network = new Network(gameID, config, signalingURL) - const player = new Player(playerName, network) - this.players.set(playerName, player) - - // Give the Network some time to connect to the signaling server. - // Giving this some time makes our test less flaky. - setTimeout(() => { - resolve(player) - }, 50) - }) - } -} -setWorldConstructor(World) - -BeforeAll(async () => await new Promise(resolve => { - let c = 2; - ['signaling', 'testproxy'].forEach(backend => { - const proc = spawn('go', ['build', '-o', `/tmp/netlib-cucumber-${backend}`, `cmd/${backend}/main.go`], { - windowsHide: true, - stdio: 'inherit' - }) - proc.on('close', () => { - if (proc.exitCode !== 0) { - console.log('failed to compile', backend) - process.exit(1) - } - if (--c === 0) { - resolve(undefined) - } - }) - }) -})) - -AfterAll(function () { - unlinkSync('/tmp/netlib-cucumber-signaling') - unlinkSync('/tmp/netlib-cucumber-testproxy') - - // node-webrtc seem to always SEGFAULT when the process is killed, this is - // a quick workaround to make sure the process is killed neatly. - // source: https://github.com/node-webrtc/node-webrtc/issues/636#issuecomment-774171409 - process.on('beforeExit', (code) => process.exit(code)) - - setTimeout(() => { - console.log('cucumber did not exit cleanly, forcing exit') - process.exit(anyScenarioFailed ? 1 : 0) - }, 2000).unref() -}) - -Before(function (this: World) { - this.scenarioRunning = true -}) -After(function (this: World, { result }) { - this.scenarioRunning = false - - if (result?.status === 'FAILED') { - anyScenarioFailed = true - } -}) diff --git a/go.mod b/go.mod index 0c7bc73..0daf52d 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,13 @@ module github.com/poki/netlib -go 1.25.0 +go 1.26.2 require ( + github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc + github.com/chromedp/chromedp v0.15.1 github.com/coder/websocket v1.8.14 + github.com/cucumber/godog v0.15.1 + github.com/cucumber/messages/go/v21 v21.0.1 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/jackc/pgx/v5 v5.9.2 github.com/koenbollen/logging v0.0.0-20230520102501-e01d64214504 @@ -22,18 +26,28 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.2.0+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect + github.com/gofrs/uuid v4.3.1+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.4 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -47,6 +61,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runc v1.2.8 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.7 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index b147452..265ae85 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,12 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc h1:wkN/LMi5vc60pBRWx6qpbk/aEvq3/ZVNpnMvsw8PVVU= +github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc/go.mod h1:cbyjALe67vDvlvdiG9369P8w5U2w6IshwtyD2f2Tvag= +github.com/chromedp/chromedp v0.15.1 h1:EJWiPm7BNqDqjYy6U0lTSL5wNH+iNt9GjC3a4gfjNyQ= +github.com/chromedp/chromedp v0.15.1/go.mod h1:CdTHtUqD/dqaFw/cvFWtTydoEQS44wLBuwbMR9EkOY4= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= @@ -20,8 +26,16 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -40,6 +54,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= +github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -49,6 +65,15 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= @@ -59,6 +84,18 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -71,10 +108,15 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/koenbollen/logging v0.0.0-20230520102501-e01d64214504 h1:4XwVIPnDZkE3EMNd5DAMedHVH+t7Ge9Lig50+EzwsD4= github.com/koenbollen/logging v0.0.0-20230520102501-e01d64214504/go.mod h1:XqaLEwx7CTcTVg3M8J4ZrWJ3W5oBUCnVcOteDzTSzVI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -95,6 +137,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -110,11 +154,22 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -152,6 +207,7 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= diff --git a/lib/peer.ts b/lib/peer.ts index 28792f4..82f0da9 100644 --- a/lib/peer.ts +++ b/lib/peer.ts @@ -64,13 +64,8 @@ export default class Peer { return } this.makingOffer = true - if (process.env.NODE_ENV === 'test') { - // Running tests with node and the wrtc package causes the - // setLocalDescription to fail with undefined as argment. - await this.conn.setLocalDescription(await this.conn.createOffer()) - } else { - await this.conn.setLocalDescription() - } + await this.conn.setLocalDescription() + await this.waitForTestProxyCandidates() const description = this.conn.localDescription if (description != null) { await this.testSessionWrapper?.(description, this.config, this.network.id, this.id) @@ -225,6 +220,28 @@ export default class Peer { void this.signaling.event('rtc', 'error', { target: this.id, error: JSON.stringify(e) }) } + private async waitForTestProxyCandidates (): Promise { + if (this.testSessionWrapper === undefined || this.conn.iceGatheringState === 'complete') { + return + } + + await new Promise(resolve => { + const done = (): void => { + clearTimeout(timeout) + this.conn.removeEventListener('icegatheringstatechange', onStateChange) + resolve() + } + const onStateChange = (): void => { + if (this.conn.iceGatheringState === 'complete') { + done() + } + } + const timeout = setTimeout(done, 5000) + this.conn.addEventListener('icegatheringstatechange', onStateChange) + onStateChange() + }) + } + /** * @internal */ @@ -258,11 +275,8 @@ export default class Peer { await this.conn.setRemoteDescription(description) this.isSettingRemoteAnswerPending = false if (description.type === 'offer') { - if (process.env.NODE_ENV === 'test') { - await this.conn.setLocalDescription(await this.conn.createAnswer()) - } else { - await this.conn.setLocalDescription() - } + await this.conn.setLocalDescription() + await this.waitForTestProxyCandidates() const description = this.conn.localDescription if (description != null) { await this.testSessionWrapper?.(description, this.config, this.network.id, this.id) @@ -305,20 +319,39 @@ async function wrapSessionDescription (desc: RTCSessionDescription, config: Peer let lines = desc.sdp.split('\r\n') lines = lines.filter(l => { - return !l.startsWith('a=candidate') || (l.includes('127.0.0.1') && l.includes('udp')) + return !l.startsWith('a=candidate') || parseProxyableCandidate(l) !== undefined }) for (let i = 0; i < lines.length; i++) { - const l = lines[i] - if (l.startsWith('a=candidate') && l.includes('127.0.0.1')) { - const orignalPort = l.split('127.0.0.1 ').pop()?.split(' ')[0] // find port - if (orignalPort != null) { - const resp = await fetch(`${config.testproxyURL}/create?id=${selfID + otherID}&port=${orignalPort}`) - const substitudePort = await resp.text() - lines[i] = l.replaceAll(` ${orignalPort} `, ` ${substitudePort} `) - } + const candidate = parseProxyableCandidate(lines[i]) + if (candidate !== undefined) { + const params = new URLSearchParams({ + id: selfID + otherID, + host: candidate.host, + port: candidate.port + }) + const resp = await fetch(`${config.testproxyURL}/create?${params.toString()}`) + const substitudePort = await resp.text() + candidate.parts[4] = '127.0.0.1' + candidate.parts[5] = substitudePort + lines[i] = candidate.parts.join(' ') } } ;(desc as any).sdp = lines.join('\r\n') } + +function parseProxyableCandidate (line: string): { parts: string[], host: string, port: string } | undefined { + if (!line.startsWith('a=candidate')) { + return undefined + } + const parts = line.split(' ') + const protocol = parts[2]?.toLowerCase() + const host = parts[4] + const port = parts[5] + const typ = parts[7] + if (protocol !== 'udp' || typ !== 'host' || host === undefined || port === undefined || host.includes(':')) { + return undefined + } + return { parts, host, port } +} diff --git a/package.json b/package.json index 2e667c7..71c2eb6 100644 --- a/package.json +++ b/package.json @@ -33,25 +33,18 @@ "prepare": "yarn build", "build": "parcel build", "lint": "ts-standard --fix", - "cucumber": "cucumber-js --require 'features/support/**/*.ts' --require-module ts-node/register --order random --retry 2 --retry-tag-filter flakey", "watch": "parcel --no-hmr example/index.html" }, "dependencies": { "eventemitter3": "^5.0.4" }, "devDependencies": { - "@cucumber/cucumber": "^12.8.1", "@parcel/packager-ts": "^2.16.4", "@parcel/transformer-typescript-types": "^2.16.4", - "@roamhq/wrtc": "^0.10.0", - "@types/node-fetch": "^2.6.11", - "@types/ws": "^8.18.1", - "node-fetch": "=2.7.0", + "@types/node": "^25.6.0", "parcel": "^2.16.4", - "ts-node": "^10.9.2", "ts-standard": "^12.0.2", - "typescript": "^6.0.2", - "ws": "^8.20.0" + "typescript": "^6.0.2" }, "engines": { "node": ">=14" diff --git a/yarn.lock b/yarn.lock index 7de4c8d..efb3d53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,168 +2,6 @@ # yarn lockfile v1 -"@babel/code-frame@^7.26.2": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" - integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== - dependencies: - "@babel/helper-validator-identifier" "^7.27.1" - js-tokens "^4.0.0" - picocolors "^1.1.1" - -"@babel/helper-validator-identifier@^7.27.1": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" - integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@cucumber/ci-environment@13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@cucumber/ci-environment/-/ci-environment-13.0.0.tgz#0a9c4e279814af864cd1591c4c16f284e14af39b" - integrity sha512-cs+3NzfNkGbcmHPddjEv4TKFiBpZRQ6WJEEufB9mw+ExS22V/4R/zpDSEG+fsJ/iSNCd6A2sATdY8PFOyY3YnA== - -"@cucumber/cucumber-expressions@19.0.0": - version "19.0.0" - resolved "https://registry.yarnpkg.com/@cucumber/cucumber-expressions/-/cucumber-expressions-19.0.0.tgz#562c932b1e6808485e4a45bf9cbcc93cdc3b1d45" - integrity sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg== - dependencies: - regexp-match-indices "1.0.2" - -"@cucumber/cucumber@^12.8.1": - version "12.8.1" - resolved "https://registry.yarnpkg.com/@cucumber/cucumber/-/cucumber-12.8.1.tgz#7873a1f7f60141d6077755bbbdc819e739fa6d4a" - integrity sha512-hCXxiStjbZsRVZlV+CMywkqBtJ6RZTQeXSBZGPHm1YoIOI6YB8pCo0KlnJMmxfKfoeUKagtQMNPnpJBXwhkUjQ== - dependencies: - "@cucumber/ci-environment" "13.0.0" - "@cucumber/cucumber-expressions" "19.0.0" - "@cucumber/gherkin" "38.0.0" - "@cucumber/gherkin-streams" "6.0.0" - "@cucumber/gherkin-utils" "11.0.0" - "@cucumber/html-formatter" "23.0.0" - "@cucumber/junit-xml-formatter" "0.13.3" - "@cucumber/message-streams" "4.1.1" - "@cucumber/messages" "32.2.0" - "@cucumber/pretty-formatter" "1.0.1" - "@cucumber/tag-expressions" "9.1.0" - assertion-error-formatter "^3.0.0" - capital-case "^1.0.4" - chalk "^4.1.2" - cli-table3 "0.6.5" - commander "^14.0.0" - debug "^4.3.4" - error-stack-parser "^2.1.4" - figures "^3.2.0" - glob "^13.0.0" - has-ansi "^4.0.1" - indent-string "^4.0.0" - is-installed-globally "^0.4.0" - is-stream "^2.0.0" - knuth-shuffle-seeded "^1.0.6" - lodash.merge "^4.6.2" - lodash.mergewith "^4.6.2" - luxon "3.7.2" - mkdirp "^3.0.0" - mz "^2.7.0" - progress "^2.0.3" - read-package-up "^12.0.0" - semver "7.7.4" - string-argv "0.3.1" - supports-color "^8.1.1" - type-fest "^4.41.0" - util-arity "^1.1.0" - yaml "^2.2.2" - yup "1.7.1" - -"@cucumber/gherkin-streams@6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@cucumber/gherkin-streams/-/gherkin-streams-6.0.0.tgz#51e78a333439c6ed3d9e731d69ad3729a749028a" - integrity sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw== - dependencies: - commander "14.0.0" - source-map-support "0.5.21" - -"@cucumber/gherkin-utils@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@cucumber/gherkin-utils/-/gherkin-utils-11.0.0.tgz#167afa559978cf6fbe2b583d3d5f9e7c4741c28a" - integrity sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA== - dependencies: - "@cucumber/gherkin" "^38.0.0" - "@cucumber/messages" "^32.0.0" - "@teppeis/multimaps" "3.0.0" - commander "14.0.2" - source-map-support "^0.5.21" - -"@cucumber/gherkin@38.0.0", "@cucumber/gherkin@^38.0.0": - version "38.0.0" - resolved "https://registry.yarnpkg.com/@cucumber/gherkin/-/gherkin-38.0.0.tgz#6c74388f95694e4c92762aeddf3d5638dbedf540" - integrity sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw== - dependencies: - "@cucumber/messages" ">=31.0.0 <33" - -"@cucumber/html-formatter@23.0.0": - version "23.0.0" - resolved "https://registry.yarnpkg.com/@cucumber/html-formatter/-/html-formatter-23.0.0.tgz#066f548f55274b58b67b4930836bd73579a9bf07" - integrity sha512-WwcRzdM8Ixy4e53j+Frm3fKM5rNuIyWUfy4HajEN+Xk/YcjA6yW0ACGTFDReB++VDZz/iUtwYdTlPRY36NbqJg== - -"@cucumber/junit-xml-formatter@0.13.3": - version "0.13.3" - resolved "https://registry.yarnpkg.com/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.13.3.tgz#a6ee049caa4afe9160f1514b8c65f24a00e71f42" - integrity sha512-w9ujOxiuKDtU6fLzJz+wp4Sgp5Xu6ba7ls00LHJccVmQU0Ba7zs+AHnv3iIgPjKZAQe1w8x93dr8Gaubh7Vqkg== - dependencies: - "@cucumber/query" "^15.0.1" - "@teppeis/multimaps" "^3.0.0" - luxon "^3.5.0" - xmlbuilder "^15.1.1" - -"@cucumber/message-streams@4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@cucumber/message-streams/-/message-streams-4.1.1.tgz#d3a271e6f6c90a52d731fb3554a56c4c6b456d17" - integrity sha512-QCAntLajesWMyX+mZKrj63YghVAts7yKFlZe46XprLbdJZN0ddB+f/Mr9OnyWKC2DHhJ18jzCfKIFCaqpAmUxg== - dependencies: - mime "^3.0.0" - -"@cucumber/messages@32.2.0", "@cucumber/messages@>=31.0.0 <33", "@cucumber/messages@^32.0.0": - version "32.2.0" - resolved "https://registry.yarnpkg.com/@cucumber/messages/-/messages-32.2.0.tgz#a6cff1646366af60e0202e934d6f43f8ccce877f" - integrity sha512-oYp1dgL2TByYWL51Z+rNm+/mFtJhiPU9WS03goes9EALb8d9GFcXRbG1JluFLFaChF1YDqIzLac0kkC3tv1DjQ== - dependencies: - class-transformer "0.5.1" - reflect-metadata "0.2.2" - -"@cucumber/pretty-formatter@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@cucumber/pretty-formatter/-/pretty-formatter-1.0.1.tgz#65d6c1df436920036a7bd02d08cb44d20e7af0ab" - integrity sha512-A1lU4VVP0aUWdOTmpdzvXOyEYuPtBDI0xYwYJnmoMDplzxMdhcHk86lyyvYDoMoPzzq6OkOE3isuosvUU4X7IQ== - dependencies: - ansi-styles "^5.0.0" - cli-table3 "^0.6.0" - figures "^3.2.0" - ts-dedent "^2.0.0" - -"@cucumber/query@^15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@cucumber/query/-/query-15.0.1.tgz#91b701121ba95caf7d0e6d9de95041ec3396d297" - integrity sha512-FMfT3orJblRsOxvU2doECBvQmauizYlj+5JsM8atAKKPbnQTj7v2/OrnuykvQpfZNBf19DYbRq1e832vllRP/g== - dependencies: - "@teppeis/multimaps" "3.0.0" - lodash.sortby "^4.7.0" - -"@cucumber/tag-expressions@9.1.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@cucumber/tag-expressions/-/tag-expressions-9.1.0.tgz#5c63cf716b6d688f140d0e4c0cc858bfd5703618" - integrity sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ== - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -215,36 +53,6 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== - dependencies: - "@isaacs/balanced-match" "^4.0.1" - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@lezer/common@^1.0.0": version "1.2.3" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd" @@ -1021,43 +829,6 @@ "@parcel/utils" "2.16.4" nullthrows "^1.1.1" -"@roamhq/wrtc-darwin-arm64@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@roamhq/wrtc-darwin-arm64/-/wrtc-darwin-arm64-0.10.0.tgz#8fd9b6eb1c7189fa4f69becef7125d56c82398e4" - integrity sha512-vFdi79jWuPHcnUcnuOjTvyKtmY/RI2xRQo9Y6RsIjIlYePN/7LTy00c+Ivrz4prYAPbp0oHscl7PDV64VUqGTQ== - -"@roamhq/wrtc-darwin-x64@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@roamhq/wrtc-darwin-x64/-/wrtc-darwin-x64-0.10.0.tgz#c860caa6997552b7d7218635f9b587e6e3900f68" - integrity sha512-H6852g2xYCuaR+/TrthpdMafs4bMfAUEpvRDhsIguzrK7Dz+MKpNI8MkwdqJN8W65J+7w7k+YqXIkTHe7Fz/cg== - -"@roamhq/wrtc-linux-arm64@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@roamhq/wrtc-linux-arm64/-/wrtc-linux-arm64-0.10.0.tgz#088f411f1d33decf530d419e4f9dab997bcc3f18" - integrity sha512-fEuJbNjprxQG6QlFd2iqBW9x028RDSho6izVg7gyt8irdPiXWOxzOxNnYMs/B2fohBTd1wD4Qxfivl07/dCR8A== - -"@roamhq/wrtc-linux-x64@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@roamhq/wrtc-linux-x64/-/wrtc-linux-x64-0.10.0.tgz#709d91ee73b66c24825498a3c39fa18fc03dc0b0" - integrity sha512-H32lK2eFg3sVb/9nkHIX5HIisxFoS82Gpesuea+zqAyRpRzSd5NpFXx28bVy9wQyRrNtj8k0bTUgEzWRzSbYCA== - -"@roamhq/wrtc-win32-x64@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@roamhq/wrtc-win32-x64/-/wrtc-win32-x64-0.10.0.tgz#ecc0f3dde8ecc8ed20263a315d385fea51af309f" - integrity sha512-wEVXMvLrBizdLyrd+Zc7zb7zpwUuHUBXwrdIvI69e3i/AA8YsVYI2xo/sxk6GoQ+o8a14ONc4SStDS35TCjg+w== - -"@roamhq/wrtc@^0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@roamhq/wrtc/-/wrtc-0.10.0.tgz#eecdfdad778e75b42c5e9f66584872c7dff4155f" - integrity sha512-yFqQQ0EV1ZUHaphh3tmjoxPi2wzhW2vjmzoAVNRRLUjXYd2e1nvwi9TKfE2w4WNvNws/hBkouvOt23Xo9FkXkQ== - optionalDependencies: - "@roamhq/wrtc-darwin-arm64" "0.10.0" - "@roamhq/wrtc-darwin-x64" "0.10.0" - "@roamhq/wrtc-linux-arm64" "0.10.0" - "@roamhq/wrtc-linux-x64" "0.10.0" - "@roamhq/wrtc-win32-x64" "0.10.0" - domexception "^4.0.0" - "@swc/core-darwin-arm64@1.11.24": version "1.11.24" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz#c9fcc9c4bad0511fed26210449556d2b33fb2d9a" @@ -1146,31 +917,6 @@ dependencies: "@swc/counter" "^0.1.3" -"@teppeis/multimaps@3.0.0", "@teppeis/multimaps@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@teppeis/multimaps/-/multimaps-3.0.0.tgz#bb9c3f8d569f589e548586fa0bbf423010ddfdc5" - integrity sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q== - -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== - "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1181,36 +927,18 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node-fetch@^2.6.11": - version "2.6.11" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" - integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== +"@types/node@^25.6.0": + version "25.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" + integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== dependencies: - "@types/node" "*" - form-data "^4.0.0" - -"@types/node@*": - version "18.16.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.1.tgz#5db121e9c5352925bb1f1b892c4ae620e3526799" - integrity sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA== - -"@types/normalize-package-data@^2.4.4": - version "2.4.4" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" - integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== + undici-types "~7.19.0" "@types/semver@^7.3.12": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== -"@types/ws@^8.18.1": - version "8.18.1" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" - integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@^5.0.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" @@ -1305,16 +1033,6 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.4.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - acorn@^8.9.0: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" @@ -1330,11 +1048,6 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-regex@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -1347,21 +1060,6 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1480,20 +1178,6 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" -assertion-error-formatter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz#be9c8825dee6a8a6c72183d915912d9b57d5d265" - integrity sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ== - dependencies: - diff "^4.0.1" - pad-right "^0.2.2" - repeat-string "^1.6.1" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -1543,11 +1227,6 @@ browserslist@^4.24.5: node-releases "^2.0.19" update-browserslist-db "^1.1.3" -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - builtins@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.1.0.tgz#6d85eeb360c4ebc166c3fdef922a15aa7316a5e8" @@ -1555,14 +1234,6 @@ builtins@^5.0.1: dependencies: semver "^7.0.0" -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -1592,15 +1263,6 @@ caniuse-lite@^1.0.30001716: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== -capital-case@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" - integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case-first "^2.0.2" - chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1619,20 +1281,6 @@ chrome-trace-event@^1.0.3: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -class-transformer@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" - integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== - -cli-table3@0.6.5, cli-table3@^0.6.0: - version "0.6.5" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - clone@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" @@ -1650,23 +1298,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.0.tgz#f244fc74a92343514e56229f16ef5c5e22ced5e9" - integrity sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA== - -commander@14.0.2, commander@^14.0.0: - version "14.0.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.2.tgz#b71fd37fe4069e4c3c7c13925252ada4eba14e8e" - integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ== - commander@^12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" @@ -1677,11 +1308,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-spawn@^7.0.2: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1763,11 +1389,6 @@ define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -1783,11 +1404,6 @@ detect-libc@^2.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1809,13 +1425,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -domexception@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" - integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== - dependencies: - webidl-conversions "^7.0.0" - dotenv-expand@^11.0.7: version "11.0.7" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-11.0.7.tgz#af695aea007d6fdc84c86cd8d0ad7beb40a0bd08" @@ -1833,25 +1442,11 @@ dotenv@^16.5.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - electron-to-chromium@^1.5.149: version "1.5.155" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz#809dd0ae9ae1db87c358e0c0c17c09a2ffc432d1" integrity sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng== -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -1859,13 +1454,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-stack-parser@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" - integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== - dependencies: - stackframe "^1.3.4" - es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" @@ -1965,11 +1553,6 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -2002,13 +1585,6 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" -es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -2027,16 +1603,6 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -2065,11 +1631,6 @@ escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -2348,13 +1909,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -figures@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2369,11 +1923,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -find-up-simple@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/find-up-simple/-/find-up-simple-1.0.1.tgz#18fb90ad49e45252c4d7fca56baade04fa3fca1e" - integrity sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ== - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -2417,17 +1966,6 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -form-data@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" - integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2488,35 +2026,11 @@ get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - get-port@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" @@ -2553,15 +2067,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3" - integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== - dependencies: - minimatch "^10.1.1" - minipass "^7.1.2" - path-scurry "^2.0.0" - glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -2574,13 +2079,6 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - globals@^13.19.0, globals@^13.24.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -2614,11 +2112,6 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - graceful-fs@^4.1.15: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -2629,13 +2122,6 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -has-ansi@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-4.0.1.tgz#f216a8c8d7b129e490dc15f4a62cc1cdb9603ce8" - integrity sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A== - dependencies: - ansi-regex "^4.1.0" - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -2675,11 +2161,6 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -2708,13 +2189,6 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hosted-git-info@^9.0.0: - version "9.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-9.0.2.tgz#b38c8a802b274e275eeeccf9f4a1b1a0a8557ada" - integrity sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg== - dependencies: - lru-cache "^11.1.0" - ignore@^5.1.1, ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" @@ -2733,16 +2207,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -index-to-position@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-1.2.0.tgz#c800eb34dacf4dbf96b9b06c7eb78d5f704138b4" - integrity sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2756,11 +2220,6 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - internal-slot@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" @@ -2868,11 +2327,6 @@ is-finalizationregistry@^1.0.2: dependencies: call-bind "^1.0.2" -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - is-generator-function@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -2887,14 +2341,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" @@ -2922,7 +2368,7 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.2, is-path-inside@^3.0.3: +is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== @@ -2954,11 +2400,6 @@ is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -3032,7 +2473,7 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -3079,13 +2520,6 @@ json5@^2.2.1, json5@^2.2.3: array-includes "^3.1.5" object.assign "^4.1.3" -knuth-shuffle-seeded@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz#01f1b65733aa7540ee08d8b0174164d22081e4e1" - integrity sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg== - dependencies: - seed-random "~2.2.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -3223,16 +2657,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.mergewith@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== - loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -3240,38 +2664,6 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lru-cache@^11.0.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117" - integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A== - -lru-cache@^11.1.0: - version "11.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" - integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== - -luxon@3.7.2, luxon@^3.5.0: - version "3.7.2" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" - integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -3285,30 +2677,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" - integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== - -minimatch@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" - integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== - dependencies: - "@isaacs/brace-expansion" "^5.0.0" - minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -3321,16 +2689,6 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -mkdirp@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -3369,15 +2727,6 @@ msgpackr@^1.9.5: optionalDependencies: msgpackr-extract "^3.0.2" -mz@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -3388,14 +2737,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - node-addon-api@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -3406,13 +2747,6 @@ node-addon-api@^6.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== -node-fetch@=2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - node-gyp-build-optional-packages@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" @@ -3437,21 +2771,12 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -normalize-package-data@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-8.0.0.tgz#bdce7ff2d6ba891b853e179e45a5337766e304a7" - integrity sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ== - dependencies: - hosted-git-info "^9.0.0" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== -object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -3599,13 +2924,6 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pad-right@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" - integrity sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g== - dependencies: - repeat-string "^1.5.2" - parcel@^2.16.4: version "2.16.4" resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.16.4.tgz#a9219e35b97163e4fbee47241d641ed23c48c800" @@ -3642,15 +2960,6 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.3.0.tgz#88a195a2157025139a2317a4f2f9252b61304ed5" - integrity sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ== - dependencies: - "@babel/code-frame" "^7.26.2" - index-to-position "^1.1.0" - type-fest "^4.39.1" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -3681,14 +2990,6 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" - integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== - dependencies: - lru-cache "^11.0.0" - minipass "^7.1.2" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -3740,11 +3041,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -3754,11 +3050,6 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" - integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== - punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -3779,31 +3070,6 @@ react-refresh@^0.16.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.16.0.tgz#e7d45625f05c9709466d09348a25d22f79b2ad23" integrity sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A== -read-package-up@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/read-package-up/-/read-package-up-12.0.0.tgz#7ae889586f397b7a291ca59ce08caf7e9f68a61c" - integrity sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw== - dependencies: - find-up-simple "^1.0.1" - read-pkg "^10.0.0" - type-fest "^5.2.0" - -read-pkg@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-10.0.0.tgz#06401f0331115e9fba9880cb3f2ae1efa3db00e4" - integrity sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A== - dependencies: - "@types/normalize-package-data" "^2.4.4" - normalize-package-data "^8.0.0" - parse-json "^8.3.0" - type-fest "^5.2.0" - unicorn-magic "^0.3.0" - -reflect-metadata@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" - integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== - reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" @@ -3822,18 +3088,6 @@ regenerator-runtime@^0.14.1: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -regexp-match-indices@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz#cf20054a6f7d5b3e116a701a7b00f82889d10da6" - integrity sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ== - dependencies: - regexp-tree "^0.1.11" - -regexp-tree@^0.1.11: - version "0.1.27" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" - integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== - regexp.prototype.flags@^1.4.3: version "1.5.0" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" @@ -3858,11 +3112,6 @@ regexpp@^3.0.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -3947,21 +3196,16 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -seed-random@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" - integrity sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ== - -semver@7.7.4, semver@^7.0.0, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.7.1: - version "7.7.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" - integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== - semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.0.0, semver@^7.3.7, semver@^7.3.8, semver@^7.7.1: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -4020,50 +3264,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -source-map-support@0.5.21, source-map-support@^0.5.21: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" - integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.18" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz#22aa922dcf2f2885a6494a261f2d8b75345d0326" - integrity sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ== - -stackframe@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" - integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== - standard-engine@^15.0.0: version "15.1.0" resolved "https://registry.yarnpkg.com/standard-engine/-/standard-engine-15.1.0.tgz#717409a002edd13cd57f6554fdd3464d9a22a774" @@ -4074,20 +3274,6 @@ standard-engine@^15.0.0: pkg-conf "^3.1.0" xdg-basedir "^4.0.0" -string-argv@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" - integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== - -string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string.prototype.matchall@^4.0.11: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" @@ -4193,23 +3379,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tagged-tag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" - integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== - term-size@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" @@ -4220,25 +3394,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -tiny-case@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" - integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -4246,40 +3401,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toposort@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -ts-dedent@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" - integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== - -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - ts-standard@^12.0.2: version "12.0.2" resolved "https://registry.yarnpkg.com/ts-standard/-/ts-standard-12.0.2.tgz#883db655106f9bde374348fc81c89e8a30414e1b" @@ -4313,7 +3434,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.4.0: +tslib@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== @@ -4342,23 +3463,6 @@ type-fest@^0.3.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== -type-fest@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-fest@^4.39.1, type-fest@^4.41.0: - version "4.41.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" - integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== - -type-fest@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.3.1.tgz#251b8d0a813c1dbccf1f9450ba5adcdf7072adc2" - integrity sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg== - dependencies: - tagged-tag "^1.0.0" - typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" @@ -4427,10 +3531,10 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unicorn-magic@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104" - integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== +undici-types@~7.19.0: + version "7.19.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" + integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== update-browserslist-db@^1.1.3: version "1.1.3" @@ -4440,13 +3544,6 @@ update-browserslist-db@^1.1.3: escalade "^3.2.0" picocolors "^1.1.1" -upper-case-first@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" - integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== - dependencies: - tslib "^2.0.3" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -4454,52 +3551,16 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-arity@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/util-arity/-/util-arity-1.1.0.tgz#59d01af1fdb3fede0ac4e632b0ab5f6ce97c9330" - integrity sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA== - utility-types@^3.11.0: version "3.11.0" resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -validate-npm-package-license@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - weak-lru-cache@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webidl-conversions@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" - integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -4579,31 +3640,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@^8.20.0: - version "8.20.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8" - integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA== - xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xmlbuilder@^15.1.1: - version "15.1.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" - integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== - -yaml@^2.2.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" - integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" @@ -4613,13 +3654,3 @@ yocto-queue@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== - -yup@1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/yup/-/yup-1.7.1.tgz#4c47c6bb367df08d4bc597f8c4c4f5fc4277f6ab" - integrity sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw== - dependencies: - property-expr "^2.0.5" - tiny-case "^1.0.3" - toposort "^2.0.2" - type-fest "^2.19.0"