offline-first, p2p synced, atproto enabled, feed reader

switch to vitest, and get a bunch of protocol tests going

-38
jest.config.js
··· 1 - import * as tsjest from 'ts-jest' 2 - import globToRegexp from 'glob-to-regexp' 3 - import path from 'node:path' 4 - import {fileURLToPath} from 'node:url' 5 - import gitignore from 'parse-gitignore' 6 - 7 - function gitignorePatterns() { 8 - const __filename = fileURLToPath(import.meta.url) 9 - const __dirname = path.dirname(__filename) 10 - const ignorefile = path.resolve(__dirname, '.gitignore') 11 - 12 - const {patterns} = gitignore(ignorefile) 13 - return patterns.map((p) => globToRegexp(p, {globstar: true}).source) 14 - } 15 - 16 - export default { 17 - testMatch: ['<rootDir>/src/**/*.spec.{ts,tsx}'], 18 - testPathIgnorePatterns: [...gitignorePatterns()], 19 - 20 - cache: true, 21 - cacheDirectory: path.join(import.meta.dirname, '.jestcache'), 22 - 23 - testEnvironment: 'node', 24 - setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], 25 - 26 - // relative imports work by default 27 - moduleNameMapper: { 28 - '\\.(css|scss|sass)$': 'identity-obj-proxy', 29 - }, 30 - 31 - // use ts-jest preset for esm support 32 - // but we have to tell it to look at jsx files too, not just tsx 33 - ...tsjest.createJsWithTsEsmPreset(), 34 - 35 - // if node_modules are ESM, we need to _include_ them from 36 - transformIgnorePatterns: ['node_modules/(?!(nanoid|jose)/)'], 37 - collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.spec.{ts,tsx}', '!src/**/node_modules/**'], 38 - }
-9
jest.setup.js
··· 1 - import {expect} from '@jest/globals' 2 - import '@testing-library/jest-dom' 3 - 4 - expect.extend({ 5 - // custom matchers 6 - // toBeValidProtocolError(received, expectedStatus) { 7 - // // custom matcher implementation 8 - // } 9 - })
+410 -3840
package-lock.json
··· 10 10 "dependencies": { 11 11 "@mozilla/readability": "^0.6.0", 12 12 "@tailwindcss/vite": "^4.1.16", 13 - "better-sqlite3": "^12.4.1", 14 13 "clsx": "^2.1.1", 15 14 "dexie": "^4.2.1", 16 15 "express": "^5.1.0", ··· 37 36 "@eslint/json": "~0.12.0", 38 37 "@eslint/markdown": "~6.5.0", 39 38 "@faker-js/faker": "^9.8.0", 40 - "@jest/globals": "^30.0.0", 41 39 "@testing-library/jest-dom": "^6.6.3", 42 40 "@trivago/prettier-plugin-sort-imports": "^6.0.0", 43 - "@types/better-sqlite3": "^7.6.13", 44 41 "@types/confusing-browser-globals": "^1.0.3", 45 42 "@types/express": "^5.0.3", 46 - "@types/jest": "^30.0.0", 47 43 "@types/level": "^6.0.3", 48 44 "@types/node": "^24.0.1", 49 45 "@types/ws": "^8.18.1", 46 + "@vitejs/plugin-basic-ssl": "^2.1.0", 47 + "@vitest/coverage-v8": "^4.0.10", 50 48 "confusing-browser-globals": "^1.0.11", 51 49 "eslint": "^9.28.0", 52 50 "eslint-config-prettier": "^10.1.5", 53 51 "eslint-plugin-prettier": "^5.5.0", 54 52 "eslint-plugin-solid": "^0.14.5", 55 53 "eslint-plugin-tsdoc": "^0.4.0", 56 - "glob-to-regexp": "^0.4.1", 57 54 "globals": "^16.2.0", 58 55 "identity-obj-proxy": "^3.0.0", 59 56 "indexeddbshim": "^16.0.0", 60 - "jest": "^30.0.2", 61 - "jest-environment-jsdom": "^30.0.0", 62 - "jest-fixed-jsdom": "^0.0.9", 63 - "jest-websocket-mock": "^2.5.0", 64 57 "jsdom": "^26.1.0", 65 58 "memfs": "^4.50.0", 66 - "parse-gitignore": "^2.0.0", 67 59 "prettier": "^3.5.3", 68 60 "prettier-plugin-organize-imports": "^4.3.0", 69 61 "solid-devtools": "^0.34.3", 70 - "ts-jest": "^29.4.0", 71 62 "tw-animate-css": "^1.4.0", 72 63 "typedoc": "^0.28.14", 73 64 "typedoc-plugin-markdown": "^4.9.0", ··· 82 73 "vite-plugin-checker": "^0.11.0", 83 74 "vite-plugin-node-polyfills": "^0.24.0", 84 75 "vite-plugin-solid": "^2.11.8", 76 + "vitest": "^4.0.10", 85 77 "wireit": "^0.14.12", 86 78 "zod-schema-faker": "^2.0.0-beta.5" 87 79 } ··· 317 309 "node": ">=6.0.0" 318 310 } 319 311 }, 320 - "node_modules/@babel/plugin-syntax-async-generators": { 321 - "version": "7.8.4", 322 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", 323 - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", 324 - "dev": true, 325 - "license": "MIT", 326 - "dependencies": { 327 - "@babel/helper-plugin-utils": "^7.8.0" 328 - }, 329 - "peerDependencies": { 330 - "@babel/core": "^7.0.0-0" 331 - } 332 - }, 333 - "node_modules/@babel/plugin-syntax-bigint": { 334 - "version": "7.8.3", 335 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", 336 - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", 337 - "dev": true, 338 - "license": "MIT", 339 - "dependencies": { 340 - "@babel/helper-plugin-utils": "^7.8.0" 341 - }, 342 - "peerDependencies": { 343 - "@babel/core": "^7.0.0-0" 344 - } 345 - }, 346 - "node_modules/@babel/plugin-syntax-class-properties": { 347 - "version": "7.12.13", 348 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", 349 - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", 350 - "dev": true, 351 - "license": "MIT", 352 - "dependencies": { 353 - "@babel/helper-plugin-utils": "^7.12.13" 354 - }, 355 - "peerDependencies": { 356 - "@babel/core": "^7.0.0-0" 357 - } 358 - }, 359 - "node_modules/@babel/plugin-syntax-class-static-block": { 360 - "version": "7.14.5", 361 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", 362 - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", 363 - "dev": true, 364 - "license": "MIT", 365 - "dependencies": { 366 - "@babel/helper-plugin-utils": "^7.14.5" 367 - }, 368 - "engines": { 369 - "node": ">=6.9.0" 370 - }, 371 - "peerDependencies": { 372 - "@babel/core": "^7.0.0-0" 373 - } 374 - }, 375 - "node_modules/@babel/plugin-syntax-import-attributes": { 376 - "version": "7.27.1", 377 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", 378 - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", 379 - "dev": true, 380 - "license": "MIT", 381 - "dependencies": { 382 - "@babel/helper-plugin-utils": "^7.27.1" 383 - }, 384 - "engines": { 385 - "node": ">=6.9.0" 386 - }, 387 - "peerDependencies": { 388 - "@babel/core": "^7.0.0-0" 389 - } 390 - }, 391 - "node_modules/@babel/plugin-syntax-import-meta": { 392 - "version": "7.10.4", 393 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", 394 - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", 395 - "dev": true, 396 - "license": "MIT", 397 - "dependencies": { 398 - "@babel/helper-plugin-utils": "^7.10.4" 399 - }, 400 - "peerDependencies": { 401 - "@babel/core": "^7.0.0-0" 402 - } 403 - }, 404 - "node_modules/@babel/plugin-syntax-json-strings": { 405 - "version": "7.8.3", 406 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", 407 - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", 408 - "dev": true, 409 - "license": "MIT", 410 - "dependencies": { 411 - "@babel/helper-plugin-utils": "^7.8.0" 412 - }, 413 - "peerDependencies": { 414 - "@babel/core": "^7.0.0-0" 415 - } 416 - }, 417 312 "node_modules/@babel/plugin-syntax-jsx": { 418 313 "version": "7.27.1", 419 314 "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", ··· 430 325 "@babel/core": "^7.0.0-0" 431 326 } 432 327 }, 433 - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { 434 - "version": "7.10.4", 435 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", 436 - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", 437 - "dev": true, 438 - "license": "MIT", 439 - "dependencies": { 440 - "@babel/helper-plugin-utils": "^7.10.4" 441 - }, 442 - "peerDependencies": { 443 - "@babel/core": "^7.0.0-0" 444 - } 445 - }, 446 - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { 447 - "version": "7.8.3", 448 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", 449 - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", 450 - "dev": true, 451 - "license": "MIT", 452 - "dependencies": { 453 - "@babel/helper-plugin-utils": "^7.8.0" 454 - }, 455 - "peerDependencies": { 456 - "@babel/core": "^7.0.0-0" 457 - } 458 - }, 459 - "node_modules/@babel/plugin-syntax-numeric-separator": { 460 - "version": "7.10.4", 461 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", 462 - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", 463 - "dev": true, 464 - "license": "MIT", 465 - "dependencies": { 466 - "@babel/helper-plugin-utils": "^7.10.4" 467 - }, 468 - "peerDependencies": { 469 - "@babel/core": "^7.0.0-0" 470 - } 471 - }, 472 - "node_modules/@babel/plugin-syntax-object-rest-spread": { 473 - "version": "7.8.3", 474 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", 475 - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", 476 - "dev": true, 477 - "license": "MIT", 478 - "dependencies": { 479 - "@babel/helper-plugin-utils": "^7.8.0" 480 - }, 481 - "peerDependencies": { 482 - "@babel/core": "^7.0.0-0" 483 - } 484 - }, 485 - "node_modules/@babel/plugin-syntax-optional-catch-binding": { 486 - "version": "7.8.3", 487 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", 488 - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", 489 - "dev": true, 490 - "license": "MIT", 491 - "dependencies": { 492 - "@babel/helper-plugin-utils": "^7.8.0" 493 - }, 494 - "peerDependencies": { 495 - "@babel/core": "^7.0.0-0" 496 - } 497 - }, 498 - "node_modules/@babel/plugin-syntax-optional-chaining": { 499 - "version": "7.8.3", 500 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", 501 - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", 502 - "dev": true, 503 - "license": "MIT", 504 - "dependencies": { 505 - "@babel/helper-plugin-utils": "^7.8.0" 506 - }, 507 - "peerDependencies": { 508 - "@babel/core": "^7.0.0-0" 509 - } 510 - }, 511 - "node_modules/@babel/plugin-syntax-private-property-in-object": { 512 - "version": "7.14.5", 513 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", 514 - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", 515 - "dev": true, 516 - "license": "MIT", 517 - "dependencies": { 518 - "@babel/helper-plugin-utils": "^7.14.5" 519 - }, 520 - "engines": { 521 - "node": ">=6.9.0" 522 - }, 523 - "peerDependencies": { 524 - "@babel/core": "^7.0.0-0" 525 - } 526 - }, 527 - "node_modules/@babel/plugin-syntax-top-level-await": { 528 - "version": "7.14.5", 529 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", 530 - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", 531 - "dev": true, 532 - "license": "MIT", 533 - "dependencies": { 534 - "@babel/helper-plugin-utils": "^7.14.5" 535 - }, 536 - "engines": { 537 - "node": ">=6.9.0" 538 - }, 539 - "peerDependencies": { 540 - "@babel/core": "^7.0.0-0" 541 - } 542 - }, 543 328 "node_modules/@babel/plugin-syntax-typescript": { 544 329 "version": "7.27.1", 545 330 "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", ··· 603 388 "engines": { 604 389 "node": ">=6.9.0" 605 390 } 606 - }, 607 - "node_modules/@bcoe/v8-coverage": { 608 - "version": "0.2.3", 609 - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", 610 - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", 611 - "dev": true, 612 - "license": "MIT" 613 391 }, 614 392 "node_modules/@csstools/color-helpers": { 615 393 "version": "5.1.0", ··· 1629 1407 "url": "https://github.com/sponsors/nzakas" 1630 1408 } 1631 1409 }, 1632 - "node_modules/@isaacs/cliui": { 1633 - "version": "8.0.2", 1634 - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 1635 - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 1636 - "dev": true, 1637 - "license": "ISC", 1638 - "dependencies": { 1639 - "string-width": "^5.1.2", 1640 - "string-width-cjs": "npm:string-width@^4.2.0", 1641 - "strip-ansi": "^7.0.1", 1642 - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 1643 - "wrap-ansi": "^8.1.0", 1644 - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 1645 - }, 1646 - "engines": { 1647 - "node": ">=12" 1648 - } 1649 - }, 1650 - "node_modules/@istanbuljs/load-nyc-config": { 1651 - "version": "1.1.0", 1652 - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", 1653 - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", 1654 - "dev": true, 1655 - "license": "ISC", 1656 - "dependencies": { 1657 - "camelcase": "^5.3.1", 1658 - "find-up": "^4.1.0", 1659 - "get-package-type": "^0.1.0", 1660 - "js-yaml": "^3.13.1", 1661 - "resolve-from": "^5.0.0" 1662 - }, 1663 - "engines": { 1664 - "node": ">=8" 1665 - } 1666 - }, 1667 - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { 1668 - "version": "1.0.10", 1669 - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 1670 - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 1671 - "dev": true, 1672 - "license": "MIT", 1673 - "dependencies": { 1674 - "sprintf-js": "~1.0.2" 1675 - } 1676 - }, 1677 - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { 1678 - "version": "4.1.0", 1679 - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 1680 - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 1681 - "dev": true, 1682 - "license": "MIT", 1683 - "dependencies": { 1684 - "locate-path": "^5.0.0", 1685 - "path-exists": "^4.0.0" 1686 - }, 1687 - "engines": { 1688 - "node": ">=8" 1689 - } 1690 - }, 1691 - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { 1692 - "version": "3.14.1", 1693 - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 1694 - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 1695 - "dev": true, 1696 - "license": "MIT", 1697 - "dependencies": { 1698 - "argparse": "^1.0.7", 1699 - "esprima": "^4.0.0" 1700 - }, 1701 - "bin": { 1702 - "js-yaml": "bin/js-yaml.js" 1703 - } 1704 - }, 1705 - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { 1706 - "version": "5.0.0", 1707 - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 1708 - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 1709 - "dev": true, 1710 - "license": "MIT", 1711 - "dependencies": { 1712 - "p-locate": "^4.1.0" 1713 - }, 1714 - "engines": { 1715 - "node": ">=8" 1716 - } 1717 - }, 1718 - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { 1719 - "version": "2.3.0", 1720 - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 1721 - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 1722 - "dev": true, 1723 - "license": "MIT", 1724 - "dependencies": { 1725 - "p-try": "^2.0.0" 1726 - }, 1727 - "engines": { 1728 - "node": ">=6" 1729 - }, 1730 - "funding": { 1731 - "url": "https://github.com/sponsors/sindresorhus" 1732 - } 1733 - }, 1734 - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { 1735 - "version": "4.1.0", 1736 - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 1737 - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 1738 - "dev": true, 1739 - "license": "MIT", 1740 - "dependencies": { 1741 - "p-limit": "^2.2.0" 1742 - }, 1743 - "engines": { 1744 - "node": ">=8" 1745 - } 1746 - }, 1747 - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { 1748 - "version": "5.0.0", 1749 - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", 1750 - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", 1751 - "dev": true, 1752 - "license": "MIT", 1753 - "engines": { 1754 - "node": ">=8" 1755 - } 1756 - }, 1757 - "node_modules/@istanbuljs/schema": { 1758 - "version": "0.1.3", 1759 - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", 1760 - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", 1761 - "dev": true, 1762 - "license": "MIT", 1763 - "engines": { 1764 - "node": ">=8" 1765 - } 1766 - }, 1767 - "node_modules/@jest/console": { 1768 - "version": "30.2.0", 1769 - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", 1770 - "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", 1771 - "dev": true, 1772 - "license": "MIT", 1773 - "dependencies": { 1774 - "@jest/types": "30.2.0", 1775 - "@types/node": "*", 1776 - "chalk": "^4.1.2", 1777 - "jest-message-util": "30.2.0", 1778 - "jest-util": "30.2.0", 1779 - "slash": "^3.0.0" 1780 - }, 1781 - "engines": { 1782 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1783 - } 1784 - }, 1785 - "node_modules/@jest/core": { 1786 - "version": "30.2.0", 1787 - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", 1788 - "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", 1789 - "dev": true, 1790 - "license": "MIT", 1791 - "dependencies": { 1792 - "@jest/console": "30.2.0", 1793 - "@jest/pattern": "30.0.1", 1794 - "@jest/reporters": "30.2.0", 1795 - "@jest/test-result": "30.2.0", 1796 - "@jest/transform": "30.2.0", 1797 - "@jest/types": "30.2.0", 1798 - "@types/node": "*", 1799 - "ansi-escapes": "^4.3.2", 1800 - "chalk": "^4.1.2", 1801 - "ci-info": "^4.2.0", 1802 - "exit-x": "^0.2.2", 1803 - "graceful-fs": "^4.2.11", 1804 - "jest-changed-files": "30.2.0", 1805 - "jest-config": "30.2.0", 1806 - "jest-haste-map": "30.2.0", 1807 - "jest-message-util": "30.2.0", 1808 - "jest-regex-util": "30.0.1", 1809 - "jest-resolve": "30.2.0", 1810 - "jest-resolve-dependencies": "30.2.0", 1811 - "jest-runner": "30.2.0", 1812 - "jest-runtime": "30.2.0", 1813 - "jest-snapshot": "30.2.0", 1814 - "jest-util": "30.2.0", 1815 - "jest-validate": "30.2.0", 1816 - "jest-watcher": "30.2.0", 1817 - "micromatch": "^4.0.8", 1818 - "pretty-format": "30.2.0", 1819 - "slash": "^3.0.0" 1820 - }, 1821 - "engines": { 1822 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1823 - }, 1824 - "peerDependencies": { 1825 - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" 1826 - }, 1827 - "peerDependenciesMeta": { 1828 - "node-notifier": { 1829 - "optional": true 1830 - } 1831 - } 1832 - }, 1833 - "node_modules/@jest/core/node_modules/ansi-styles": { 1834 - "version": "5.2.0", 1835 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 1836 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 1837 - "dev": true, 1838 - "license": "MIT", 1839 - "engines": { 1840 - "node": ">=10" 1841 - }, 1842 - "funding": { 1843 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1844 - } 1845 - }, 1846 - "node_modules/@jest/core/node_modules/pretty-format": { 1847 - "version": "30.2.0", 1848 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 1849 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 1850 - "dev": true, 1851 - "license": "MIT", 1852 - "dependencies": { 1853 - "@jest/schemas": "30.0.5", 1854 - "ansi-styles": "^5.2.0", 1855 - "react-is": "^18.3.1" 1856 - }, 1857 - "engines": { 1858 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1859 - } 1860 - }, 1861 - "node_modules/@jest/core/node_modules/react-is": { 1862 - "version": "18.3.1", 1863 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 1864 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 1865 - "dev": true, 1866 - "license": "MIT" 1867 - }, 1868 - "node_modules/@jest/diff-sequences": { 1869 - "version": "30.0.1", 1870 - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", 1871 - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", 1872 - "dev": true, 1873 - "license": "MIT", 1874 - "engines": { 1875 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1876 - } 1877 - }, 1878 - "node_modules/@jest/environment": { 1879 - "version": "30.2.0", 1880 - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", 1881 - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", 1882 - "dev": true, 1883 - "license": "MIT", 1884 - "dependencies": { 1885 - "@jest/fake-timers": "30.2.0", 1886 - "@jest/types": "30.2.0", 1887 - "@types/node": "*", 1888 - "jest-mock": "30.2.0" 1889 - }, 1890 - "engines": { 1891 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1892 - } 1893 - }, 1894 - "node_modules/@jest/environment-jsdom-abstract": { 1895 - "version": "30.2.0", 1896 - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", 1897 - "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", 1898 - "dev": true, 1899 - "license": "MIT", 1900 - "dependencies": { 1901 - "@jest/environment": "30.2.0", 1902 - "@jest/fake-timers": "30.2.0", 1903 - "@jest/types": "30.2.0", 1904 - "@types/jsdom": "^21.1.7", 1905 - "@types/node": "*", 1906 - "jest-mock": "30.2.0", 1907 - "jest-util": "30.2.0" 1908 - }, 1909 - "engines": { 1910 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1911 - }, 1912 - "peerDependencies": { 1913 - "canvas": "^3.0.0", 1914 - "jsdom": "*" 1915 - }, 1916 - "peerDependenciesMeta": { 1917 - "canvas": { 1918 - "optional": true 1919 - } 1920 - } 1921 - }, 1922 - "node_modules/@jest/expect": { 1923 - "version": "30.2.0", 1924 - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", 1925 - "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", 1926 - "dev": true, 1927 - "license": "MIT", 1928 - "dependencies": { 1929 - "expect": "30.2.0", 1930 - "jest-snapshot": "30.2.0" 1931 - }, 1932 - "engines": { 1933 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1934 - } 1935 - }, 1936 - "node_modules/@jest/expect-utils": { 1937 - "version": "30.2.0", 1938 - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", 1939 - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", 1940 - "dev": true, 1941 - "license": "MIT", 1942 - "dependencies": { 1943 - "@jest/get-type": "30.1.0" 1944 - }, 1945 - "engines": { 1946 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1947 - } 1948 - }, 1949 - "node_modules/@jest/fake-timers": { 1950 - "version": "30.2.0", 1951 - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", 1952 - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", 1953 - "dev": true, 1954 - "license": "MIT", 1955 - "dependencies": { 1956 - "@jest/types": "30.2.0", 1957 - "@sinonjs/fake-timers": "^13.0.0", 1958 - "@types/node": "*", 1959 - "jest-message-util": "30.2.0", 1960 - "jest-mock": "30.2.0", 1961 - "jest-util": "30.2.0" 1962 - }, 1963 - "engines": { 1964 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1965 - } 1966 - }, 1967 - "node_modules/@jest/get-type": { 1968 - "version": "30.1.0", 1969 - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", 1970 - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", 1971 - "dev": true, 1972 - "license": "MIT", 1973 - "engines": { 1974 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1975 - } 1976 - }, 1977 - "node_modules/@jest/globals": { 1978 - "version": "30.2.0", 1979 - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", 1980 - "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", 1981 - "dev": true, 1982 - "license": "MIT", 1983 - "dependencies": { 1984 - "@jest/environment": "30.2.0", 1985 - "@jest/expect": "30.2.0", 1986 - "@jest/types": "30.2.0", 1987 - "jest-mock": "30.2.0" 1988 - }, 1989 - "engines": { 1990 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 1991 - } 1992 - }, 1993 - "node_modules/@jest/pattern": { 1994 - "version": "30.0.1", 1995 - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", 1996 - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", 1997 - "dev": true, 1998 - "license": "MIT", 1999 - "dependencies": { 2000 - "@types/node": "*", 2001 - "jest-regex-util": "30.0.1" 2002 - }, 2003 - "engines": { 2004 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2005 - } 2006 - }, 2007 - "node_modules/@jest/reporters": { 2008 - "version": "30.2.0", 2009 - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", 2010 - "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", 2011 - "dev": true, 2012 - "license": "MIT", 2013 - "dependencies": { 2014 - "@bcoe/v8-coverage": "^0.2.3", 2015 - "@jest/console": "30.2.0", 2016 - "@jest/test-result": "30.2.0", 2017 - "@jest/transform": "30.2.0", 2018 - "@jest/types": "30.2.0", 2019 - "@jridgewell/trace-mapping": "^0.3.25", 2020 - "@types/node": "*", 2021 - "chalk": "^4.1.2", 2022 - "collect-v8-coverage": "^1.0.2", 2023 - "exit-x": "^0.2.2", 2024 - "glob": "^10.3.10", 2025 - "graceful-fs": "^4.2.11", 2026 - "istanbul-lib-coverage": "^3.0.0", 2027 - "istanbul-lib-instrument": "^6.0.0", 2028 - "istanbul-lib-report": "^3.0.0", 2029 - "istanbul-lib-source-maps": "^5.0.0", 2030 - "istanbul-reports": "^3.1.3", 2031 - "jest-message-util": "30.2.0", 2032 - "jest-util": "30.2.0", 2033 - "jest-worker": "30.2.0", 2034 - "slash": "^3.0.0", 2035 - "string-length": "^4.0.2", 2036 - "v8-to-istanbul": "^9.0.1" 2037 - }, 2038 - "engines": { 2039 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2040 - }, 2041 - "peerDependencies": { 2042 - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" 2043 - }, 2044 - "peerDependenciesMeta": { 2045 - "node-notifier": { 2046 - "optional": true 2047 - } 2048 - } 2049 - }, 2050 - "node_modules/@jest/schemas": { 2051 - "version": "30.0.5", 2052 - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", 2053 - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", 2054 - "dev": true, 2055 - "license": "MIT", 2056 - "dependencies": { 2057 - "@sinclair/typebox": "^0.34.0" 2058 - }, 2059 - "engines": { 2060 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2061 - } 2062 - }, 2063 - "node_modules/@jest/snapshot-utils": { 2064 - "version": "30.2.0", 2065 - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", 2066 - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", 2067 - "dev": true, 2068 - "license": "MIT", 2069 - "dependencies": { 2070 - "@jest/types": "30.2.0", 2071 - "chalk": "^4.1.2", 2072 - "graceful-fs": "^4.2.11", 2073 - "natural-compare": "^1.4.0" 2074 - }, 2075 - "engines": { 2076 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2077 - } 2078 - }, 2079 - "node_modules/@jest/source-map": { 2080 - "version": "30.0.1", 2081 - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", 2082 - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", 2083 - "dev": true, 2084 - "license": "MIT", 2085 - "dependencies": { 2086 - "@jridgewell/trace-mapping": "^0.3.25", 2087 - "callsites": "^3.1.0", 2088 - "graceful-fs": "^4.2.11" 2089 - }, 2090 - "engines": { 2091 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2092 - } 2093 - }, 2094 - "node_modules/@jest/test-result": { 2095 - "version": "30.2.0", 2096 - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", 2097 - "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", 2098 - "dev": true, 2099 - "license": "MIT", 2100 - "dependencies": { 2101 - "@jest/console": "30.2.0", 2102 - "@jest/types": "30.2.0", 2103 - "@types/istanbul-lib-coverage": "^2.0.6", 2104 - "collect-v8-coverage": "^1.0.2" 2105 - }, 2106 - "engines": { 2107 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2108 - } 2109 - }, 2110 - "node_modules/@jest/test-sequencer": { 2111 - "version": "30.2.0", 2112 - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", 2113 - "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", 2114 - "dev": true, 2115 - "license": "MIT", 2116 - "dependencies": { 2117 - "@jest/test-result": "30.2.0", 2118 - "graceful-fs": "^4.2.11", 2119 - "jest-haste-map": "30.2.0", 2120 - "slash": "^3.0.0" 2121 - }, 2122 - "engines": { 2123 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2124 - } 2125 - }, 2126 - "node_modules/@jest/transform": { 2127 - "version": "30.2.0", 2128 - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", 2129 - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", 2130 - "dev": true, 2131 - "license": "MIT", 2132 - "dependencies": { 2133 - "@babel/core": "^7.27.4", 2134 - "@jest/types": "30.2.0", 2135 - "@jridgewell/trace-mapping": "^0.3.25", 2136 - "babel-plugin-istanbul": "^7.0.1", 2137 - "chalk": "^4.1.2", 2138 - "convert-source-map": "^2.0.0", 2139 - "fast-json-stable-stringify": "^2.1.0", 2140 - "graceful-fs": "^4.2.11", 2141 - "jest-haste-map": "30.2.0", 2142 - "jest-regex-util": "30.0.1", 2143 - "jest-util": "30.2.0", 2144 - "micromatch": "^4.0.8", 2145 - "pirates": "^4.0.7", 2146 - "slash": "^3.0.0", 2147 - "write-file-atomic": "^5.0.1" 2148 - }, 2149 - "engines": { 2150 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2151 - } 2152 - }, 2153 - "node_modules/@jest/types": { 2154 - "version": "30.2.0", 2155 - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", 2156 - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", 2157 - "dev": true, 2158 - "license": "MIT", 2159 - "dependencies": { 2160 - "@jest/pattern": "30.0.1", 2161 - "@jest/schemas": "30.0.5", 2162 - "@types/istanbul-lib-coverage": "^2.0.6", 2163 - "@types/istanbul-reports": "^3.0.4", 2164 - "@types/node": "*", 2165 - "@types/yargs": "^17.0.33", 2166 - "chalk": "^4.1.2" 2167 - }, 2168 - "engines": { 2169 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 2170 - } 2171 - }, 2172 1410 "node_modules/@jridgewell/gen-mapping": { 2173 1411 "version": "0.3.13", 2174 1412 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", ··· 2569 1807 }, 2570 1808 "engines": { 2571 1809 "node": ">=10" 2572 - } 2573 - }, 2574 - "node_modules/@pkgjs/parseargs": { 2575 - "version": "0.11.0", 2576 - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 2577 - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 2578 - "dev": true, 2579 - "license": "MIT", 2580 - "optional": true, 2581 - "engines": { 2582 - "node": ">=14" 2583 1810 } 2584 1811 }, 2585 1812 "node_modules/@pkgr/core": { ··· 3006 2233 "node": ">=10.0.0" 3007 2234 } 3008 2235 }, 3009 - "node_modules/@sinclair/typebox": { 3010 - "version": "0.34.41", 3011 - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", 3012 - "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", 3013 - "dev": true, 3014 - "license": "MIT" 3015 - }, 3016 - "node_modules/@sinonjs/commons": { 3017 - "version": "3.0.1", 3018 - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", 3019 - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", 3020 - "dev": true, 3021 - "license": "BSD-3-Clause", 3022 - "dependencies": { 3023 - "type-detect": "4.0.8" 3024 - } 3025 - }, 3026 - "node_modules/@sinonjs/fake-timers": { 3027 - "version": "13.0.5", 3028 - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", 3029 - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", 3030 - "dev": true, 3031 - "license": "BSD-3-Clause", 3032 - "dependencies": { 3033 - "@sinonjs/commons": "^3.0.1" 3034 - } 3035 - }, 3036 2236 "node_modules/@solid-devtools/debugger": { 3037 2237 "version": "0.28.1", 3038 2238 "resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.1.tgz", ··· 3242 2442 "peerDependencies": { 3243 2443 "solid-js": "^1.6.12" 3244 2444 } 2445 + }, 2446 + "node_modules/@standard-schema/spec": { 2447 + "version": "1.0.0", 2448 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", 2449 + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", 2450 + "dev": true, 2451 + "license": "MIT" 3245 2452 }, 3246 2453 "node_modules/@tailwindcss/node": { 3247 2454 "version": "4.1.16", ··· 3685 2892 "@babel/types": "^7.28.2" 3686 2893 } 3687 2894 }, 3688 - "node_modules/@types/better-sqlite3": { 3689 - "version": "7.6.13", 3690 - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", 3691 - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", 2895 + "node_modules/@types/body-parser": { 2896 + "version": "1.19.6", 2897 + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", 2898 + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", 3692 2899 "dev": true, 3693 2900 "license": "MIT", 3694 2901 "dependencies": { 2902 + "@types/connect": "*", 3695 2903 "@types/node": "*" 3696 2904 } 3697 2905 }, 3698 - "node_modules/@types/body-parser": { 3699 - "version": "1.19.6", 3700 - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", 3701 - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", 2906 + "node_modules/@types/chai": { 2907 + "version": "5.2.3", 2908 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", 2909 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", 3702 2910 "dev": true, 3703 2911 "license": "MIT", 3704 2912 "dependencies": { 3705 - "@types/connect": "*", 3706 - "@types/node": "*" 2913 + "@types/deep-eql": "*", 2914 + "assertion-error": "^2.0.1" 3707 2915 } 3708 2916 }, 3709 2917 "node_modules/@types/confusing-browser-globals": { ··· 3733 2941 "@types/ms": "*" 3734 2942 } 3735 2943 }, 2944 + "node_modules/@types/deep-eql": { 2945 + "version": "4.0.2", 2946 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 2947 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 2948 + "dev": true, 2949 + "license": "MIT" 2950 + }, 3736 2951 "node_modules/@types/encoding-down": { 3737 2952 "version": "5.0.5", 3738 2953 "resolved": "https://registry.npmjs.org/@types/encoding-down/-/encoding-down-5.0.5.tgz", ··· 3792 3007 "dev": true, 3793 3008 "license": "MIT" 3794 3009 }, 3795 - "node_modules/@types/istanbul-lib-coverage": { 3796 - "version": "2.0.6", 3797 - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", 3798 - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", 3799 - "dev": true, 3800 - "license": "MIT" 3801 - }, 3802 - "node_modules/@types/istanbul-lib-report": { 3803 - "version": "3.0.3", 3804 - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", 3805 - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", 3806 - "dev": true, 3807 - "license": "MIT", 3808 - "dependencies": { 3809 - "@types/istanbul-lib-coverage": "*" 3810 - } 3811 - }, 3812 - "node_modules/@types/istanbul-reports": { 3813 - "version": "3.0.4", 3814 - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", 3815 - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", 3816 - "dev": true, 3817 - "license": "MIT", 3818 - "dependencies": { 3819 - "@types/istanbul-lib-report": "*" 3820 - } 3821 - }, 3822 - "node_modules/@types/jest": { 3823 - "version": "30.0.0", 3824 - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", 3825 - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", 3826 - "dev": true, 3827 - "license": "MIT", 3828 - "dependencies": { 3829 - "expect": "^30.0.0", 3830 - "pretty-format": "^30.0.0" 3831 - } 3832 - }, 3833 - "node_modules/@types/jest/node_modules/ansi-styles": { 3834 - "version": "5.2.0", 3835 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 3836 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 3837 - "dev": true, 3838 - "license": "MIT", 3839 - "engines": { 3840 - "node": ">=10" 3841 - }, 3842 - "funding": { 3843 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 3844 - } 3845 - }, 3846 - "node_modules/@types/jest/node_modules/pretty-format": { 3847 - "version": "30.2.0", 3848 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 3849 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 3850 - "dev": true, 3851 - "license": "MIT", 3852 - "dependencies": { 3853 - "@jest/schemas": "30.0.5", 3854 - "ansi-styles": "^5.2.0", 3855 - "react-is": "^18.3.1" 3856 - }, 3857 - "engines": { 3858 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 3859 - } 3860 - }, 3861 - "node_modules/@types/jest/node_modules/react-is": { 3862 - "version": "18.3.1", 3863 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 3864 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 3865 - "dev": true, 3866 - "license": "MIT" 3867 - }, 3868 - "node_modules/@types/jsdom": { 3869 - "version": "21.1.7", 3870 - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", 3871 - "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", 3872 - "dev": true, 3873 - "license": "MIT", 3874 - "dependencies": { 3875 - "@types/node": "*", 3876 - "@types/tough-cookie": "*", 3877 - "parse5": "^7.0.0" 3878 - } 3879 - }, 3880 3010 "node_modules/@types/json-schema": { 3881 3011 "version": "7.0.15", 3882 3012 "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", ··· 4003 3133 "@types/node": "*" 4004 3134 } 4005 3135 }, 4006 - "node_modules/@types/stack-utils": { 4007 - "version": "2.0.3", 4008 - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", 4009 - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", 4010 - "dev": true, 4011 - "license": "MIT" 4012 - }, 4013 - "node_modules/@types/tough-cookie": { 4014 - "version": "4.0.5", 4015 - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", 4016 - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", 4017 - "dev": true, 4018 - "license": "MIT" 4019 - }, 4020 3136 "node_modules/@types/unist": { 4021 3137 "version": "3.0.3", 4022 3138 "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", ··· 4033 3149 "dependencies": { 4034 3150 "@types/node": "*" 4035 3151 } 4036 - }, 4037 - "node_modules/@types/yargs": { 4038 - "version": "17.0.34", 4039 - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", 4040 - "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", 4041 - "dev": true, 4042 - "license": "MIT", 4043 - "dependencies": { 4044 - "@types/yargs-parser": "*" 4045 - } 4046 - }, 4047 - "node_modules/@types/yargs-parser": { 4048 - "version": "21.0.3", 4049 - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", 4050 - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", 4051 - "dev": true, 4052 - "license": "MIT" 4053 3152 }, 4054 3153 "node_modules/@typescript-eslint/eslint-plugin": { 4055 3154 "version": "8.46.3", ··· 4297 3396 "url": "https://opencollective.com/typescript-eslint" 4298 3397 } 4299 3398 }, 4300 - "node_modules/@ungap/structured-clone": { 4301 - "version": "1.3.0", 4302 - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", 4303 - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", 4304 - "dev": true, 4305 - "license": "ISC" 4306 - }, 4307 - "node_modules/@unrs/resolver-binding-android-arm-eabi": { 4308 - "version": "1.11.1", 4309 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", 4310 - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", 4311 - "cpu": [ 4312 - "arm" 4313 - ], 3399 + "node_modules/@vitejs/plugin-basic-ssl": { 3400 + "version": "2.1.0", 3401 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", 3402 + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", 4314 3403 "dev": true, 4315 3404 "license": "MIT", 4316 - "optional": true, 4317 - "os": [ 4318 - "android" 4319 - ] 3405 + "engines": { 3406 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 3407 + }, 3408 + "peerDependencies": { 3409 + "vite": "^6.0.0 || ^7.0.0" 3410 + } 4320 3411 }, 4321 - "node_modules/@unrs/resolver-binding-android-arm64": { 4322 - "version": "1.11.1", 4323 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", 4324 - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", 4325 - "cpu": [ 4326 - "arm64" 4327 - ], 3412 + "node_modules/@vitest/coverage-v8": { 3413 + "version": "4.0.10", 3414 + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.10.tgz", 3415 + "integrity": "sha512-g+brmtoKa/sAeIohNJnnWhnHtU6GuqqVOSQ4SxDIPcgZWZyhJs5RmF5LpqXs8Kq64lANP+vnbn5JLzhLj/G56g==", 4328 3416 "dev": true, 4329 3417 "license": "MIT", 4330 - "optional": true, 4331 - "os": [ 4332 - "android" 4333 - ] 3418 + "dependencies": { 3419 + "@bcoe/v8-coverage": "^1.0.2", 3420 + "@vitest/utils": "4.0.10", 3421 + "ast-v8-to-istanbul": "^0.3.8", 3422 + "debug": "^4.4.3", 3423 + "istanbul-lib-coverage": "^3.2.2", 3424 + "istanbul-lib-report": "^3.0.1", 3425 + "istanbul-lib-source-maps": "^5.0.6", 3426 + "istanbul-reports": "^3.2.0", 3427 + "magicast": "^0.5.1", 3428 + "std-env": "^3.10.0", 3429 + "tinyrainbow": "^3.0.3" 3430 + }, 3431 + "funding": { 3432 + "url": "https://opencollective.com/vitest" 3433 + }, 3434 + "peerDependencies": { 3435 + "@vitest/browser": "4.0.10", 3436 + "vitest": "4.0.10" 3437 + }, 3438 + "peerDependenciesMeta": { 3439 + "@vitest/browser": { 3440 + "optional": true 3441 + } 3442 + } 4334 3443 }, 4335 - "node_modules/@unrs/resolver-binding-darwin-arm64": { 4336 - "version": "1.11.1", 4337 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", 4338 - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", 4339 - "cpu": [ 4340 - "arm64" 4341 - ], 3444 + "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { 3445 + "version": "1.0.2", 3446 + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", 3447 + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", 4342 3448 "dev": true, 4343 3449 "license": "MIT", 4344 - "optional": true, 4345 - "os": [ 4346 - "darwin" 4347 - ] 3450 + "engines": { 3451 + "node": ">=18" 3452 + } 4348 3453 }, 4349 - "node_modules/@unrs/resolver-binding-darwin-x64": { 4350 - "version": "1.11.1", 4351 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", 4352 - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", 4353 - "cpu": [ 4354 - "x64" 4355 - ], 3454 + "node_modules/@vitest/expect": { 3455 + "version": "4.0.10", 3456 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.10.tgz", 3457 + "integrity": "sha512-3QkTX/lK39FBNwARCQRSQr0TP9+ywSdxSX+LgbJ2M1WmveXP72anTbnp2yl5fH+dU6SUmBzNMrDHs80G8G2DZg==", 4356 3458 "dev": true, 4357 3459 "license": "MIT", 4358 - "optional": true, 4359 - "os": [ 4360 - "darwin" 4361 - ] 3460 + "dependencies": { 3461 + "@standard-schema/spec": "^1.0.0", 3462 + "@types/chai": "^5.2.2", 3463 + "@vitest/spy": "4.0.10", 3464 + "@vitest/utils": "4.0.10", 3465 + "chai": "^6.2.1", 3466 + "tinyrainbow": "^3.0.3" 3467 + }, 3468 + "funding": { 3469 + "url": "https://opencollective.com/vitest" 3470 + } 4362 3471 }, 4363 - "node_modules/@unrs/resolver-binding-freebsd-x64": { 4364 - "version": "1.11.1", 4365 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", 4366 - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", 4367 - "cpu": [ 4368 - "x64" 4369 - ], 3472 + "node_modules/@vitest/mocker": { 3473 + "version": "4.0.10", 3474 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.10.tgz", 3475 + "integrity": "sha512-e2OfdexYkjkg8Hh3L9NVEfbwGXq5IZbDovkf30qW2tOh7Rh9sVtmSr2ztEXOFbymNxS4qjzLXUQIvATvN4B+lg==", 4370 3476 "dev": true, 4371 3477 "license": "MIT", 4372 - "optional": true, 4373 - "os": [ 4374 - "freebsd" 4375 - ] 4376 - }, 4377 - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { 4378 - "version": "1.11.1", 4379 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", 4380 - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", 4381 - "cpu": [ 4382 - "arm" 4383 - ], 4384 - "dev": true, 4385 - "license": "MIT", 4386 - "optional": true, 4387 - "os": [ 4388 - "linux" 4389 - ] 4390 - }, 4391 - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { 4392 - "version": "1.11.1", 4393 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", 4394 - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", 4395 - "cpu": [ 4396 - "arm" 4397 - ], 4398 - "dev": true, 4399 - "license": "MIT", 4400 - "optional": true, 4401 - "os": [ 4402 - "linux" 4403 - ] 4404 - }, 4405 - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { 4406 - "version": "1.11.1", 4407 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", 4408 - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", 4409 - "cpu": [ 4410 - "arm64" 4411 - ], 4412 - "dev": true, 4413 - "license": "MIT", 4414 - "optional": true, 4415 - "os": [ 4416 - "linux" 4417 - ] 4418 - }, 4419 - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { 4420 - "version": "1.11.1", 4421 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", 4422 - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", 4423 - "cpu": [ 4424 - "arm64" 4425 - ], 4426 - "dev": true, 4427 - "license": "MIT", 4428 - "optional": true, 4429 - "os": [ 4430 - "linux" 4431 - ] 4432 - }, 4433 - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { 4434 - "version": "1.11.1", 4435 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", 4436 - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", 4437 - "cpu": [ 4438 - "ppc64" 4439 - ], 4440 - "dev": true, 4441 - "license": "MIT", 4442 - "optional": true, 4443 - "os": [ 4444 - "linux" 4445 - ] 4446 - }, 4447 - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { 4448 - "version": "1.11.1", 4449 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", 4450 - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", 4451 - "cpu": [ 4452 - "riscv64" 4453 - ], 4454 - "dev": true, 4455 - "license": "MIT", 4456 - "optional": true, 4457 - "os": [ 4458 - "linux" 4459 - ] 4460 - }, 4461 - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { 4462 - "version": "1.11.1", 4463 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", 4464 - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", 4465 - "cpu": [ 4466 - "riscv64" 4467 - ], 4468 - "dev": true, 4469 - "license": "MIT", 4470 - "optional": true, 4471 - "os": [ 4472 - "linux" 4473 - ] 3478 + "dependencies": { 3479 + "@vitest/spy": "4.0.10", 3480 + "estree-walker": "^3.0.3", 3481 + "magic-string": "^0.30.21" 3482 + }, 3483 + "funding": { 3484 + "url": "https://opencollective.com/vitest" 3485 + }, 3486 + "peerDependencies": { 3487 + "msw": "^2.4.9", 3488 + "vite": "^6.0.0 || ^7.0.0-0" 3489 + }, 3490 + "peerDependenciesMeta": { 3491 + "msw": { 3492 + "optional": true 3493 + }, 3494 + "vite": { 3495 + "optional": true 3496 + } 3497 + } 4474 3498 }, 4475 - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { 4476 - "version": "1.11.1", 4477 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", 4478 - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", 4479 - "cpu": [ 4480 - "s390x" 4481 - ], 3499 + "node_modules/@vitest/mocker/node_modules/estree-walker": { 3500 + "version": "3.0.3", 3501 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 3502 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 4482 3503 "dev": true, 4483 3504 "license": "MIT", 4484 - "optional": true, 4485 - "os": [ 4486 - "linux" 4487 - ] 3505 + "dependencies": { 3506 + "@types/estree": "^1.0.0" 3507 + } 4488 3508 }, 4489 - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { 4490 - "version": "1.11.1", 4491 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", 4492 - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", 4493 - "cpu": [ 4494 - "x64" 4495 - ], 3509 + "node_modules/@vitest/pretty-format": { 3510 + "version": "4.0.10", 3511 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.10.tgz", 3512 + "integrity": "sha512-99EQbpa/zuDnvVjthwz5bH9o8iPefoQZ63WV8+bsRJZNw3qQSvSltfut8yu1Jc9mqOYi7pEbsKxYTi/rjaq6PA==", 4496 3513 "dev": true, 4497 3514 "license": "MIT", 4498 - "optional": true, 4499 - "os": [ 4500 - "linux" 4501 - ] 3515 + "dependencies": { 3516 + "tinyrainbow": "^3.0.3" 3517 + }, 3518 + "funding": { 3519 + "url": "https://opencollective.com/vitest" 3520 + } 4502 3521 }, 4503 - "node_modules/@unrs/resolver-binding-linux-x64-musl": { 4504 - "version": "1.11.1", 4505 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", 4506 - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", 4507 - "cpu": [ 4508 - "x64" 4509 - ], 3522 + "node_modules/@vitest/runner": { 3523 + "version": "4.0.10", 3524 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.10.tgz", 3525 + "integrity": "sha512-EXU2iSkKvNwtlL8L8doCpkyclw0mc/t4t9SeOnfOFPyqLmQwuceMPA4zJBa6jw0MKsZYbw7kAn+gl7HxrlB8UQ==", 4510 3526 "dev": true, 4511 3527 "license": "MIT", 4512 - "optional": true, 4513 - "os": [ 4514 - "linux" 4515 - ] 3528 + "dependencies": { 3529 + "@vitest/utils": "4.0.10", 3530 + "pathe": "^2.0.3" 3531 + }, 3532 + "funding": { 3533 + "url": "https://opencollective.com/vitest" 3534 + } 4516 3535 }, 4517 - "node_modules/@unrs/resolver-binding-wasm32-wasi": { 4518 - "version": "1.11.1", 4519 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", 4520 - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", 4521 - "cpu": [ 4522 - "wasm32" 4523 - ], 3536 + "node_modules/@vitest/snapshot": { 3537 + "version": "4.0.10", 3538 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.10.tgz", 3539 + "integrity": "sha512-2N4X2ZZl7kZw0qeGdQ41H0KND96L3qX1RgwuCfy6oUsF2ISGD/HpSbmms+CkIOsQmg2kulwfhJ4CI0asnZlvkg==", 4524 3540 "dev": true, 4525 3541 "license": "MIT", 4526 - "optional": true, 4527 3542 "dependencies": { 4528 - "@napi-rs/wasm-runtime": "^0.2.11" 3543 + "@vitest/pretty-format": "4.0.10", 3544 + "magic-string": "^0.30.21", 3545 + "pathe": "^2.0.3" 4529 3546 }, 4530 - "engines": { 4531 - "node": ">=14.0.0" 3547 + "funding": { 3548 + "url": "https://opencollective.com/vitest" 4532 3549 } 4533 3550 }, 4534 - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { 4535 - "version": "1.11.1", 4536 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", 4537 - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", 4538 - "cpu": [ 4539 - "arm64" 4540 - ], 3551 + "node_modules/@vitest/spy": { 3552 + "version": "4.0.10", 3553 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.10.tgz", 3554 + "integrity": "sha512-AsY6sVS8OLb96GV5RoG8B6I35GAbNrC49AO+jNRF9YVGb/g9t+hzNm1H6kD0NDp8tt7VJLs6hb7YMkDXqu03iw==", 4541 3555 "dev": true, 4542 3556 "license": "MIT", 4543 - "optional": true, 4544 - "os": [ 4545 - "win32" 4546 - ] 3557 + "funding": { 3558 + "url": "https://opencollective.com/vitest" 3559 + } 4547 3560 }, 4548 - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { 4549 - "version": "1.11.1", 4550 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", 4551 - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", 4552 - "cpu": [ 4553 - "ia32" 4554 - ], 3561 + "node_modules/@vitest/utils": { 3562 + "version": "4.0.10", 3563 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.10.tgz", 3564 + "integrity": "sha512-kOuqWnEwZNtQxMKg3WmPK1vmhZu9WcoX69iwWjVz+jvKTsF1emzsv3eoPcDr6ykA3qP2bsCQE7CwqfNtAVzsmg==", 4555 3565 "dev": true, 4556 3566 "license": "MIT", 4557 - "optional": true, 4558 - "os": [ 4559 - "win32" 4560 - ] 4561 - }, 4562 - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { 4563 - "version": "1.11.1", 4564 - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", 4565 - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", 4566 - "cpu": [ 4567 - "x64" 4568 - ], 4569 - "dev": true, 4570 - "license": "MIT", 4571 - "optional": true, 4572 - "os": [ 4573 - "win32" 4574 - ] 3567 + "dependencies": { 3568 + "@vitest/pretty-format": "4.0.10", 3569 + "tinyrainbow": "^3.0.3" 3570 + }, 3571 + "funding": { 3572 + "url": "https://opencollective.com/vitest" 3573 + } 4575 3574 }, 4576 3575 "node_modules/@webtorrent/http-node": { 4577 3576 "version": "1.3.0", ··· 4722 3721 "url": "https://github.com/sponsors/epoberezkin" 4723 3722 } 4724 3723 }, 4725 - "node_modules/ansi-escapes": { 4726 - "version": "4.3.2", 4727 - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", 4728 - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", 4729 - "dev": true, 4730 - "license": "MIT", 4731 - "dependencies": { 4732 - "type-fest": "^0.21.3" 4733 - }, 4734 - "engines": { 4735 - "node": ">=8" 4736 - }, 4737 - "funding": { 4738 - "url": "https://github.com/sponsors/sindresorhus" 4739 - } 4740 - }, 4741 3724 "node_modules/ansi-regex": { 4742 3725 "version": "5.0.1", 4743 3726 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 4744 3727 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 4745 3728 "dev": true, 4746 3729 "license": "MIT", 3730 + "optional": true, 4747 3731 "engines": { 4748 3732 "node": ">=8" 4749 3733 } ··· 4859 3843 "util": "^0.12.5" 4860 3844 } 4861 3845 }, 3846 + "node_modules/assertion-error": { 3847 + "version": "2.0.1", 3848 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 3849 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 3850 + "dev": true, 3851 + "license": "MIT", 3852 + "engines": { 3853 + "node": ">=12" 3854 + } 3855 + }, 3856 + "node_modules/ast-v8-to-istanbul": { 3857 + "version": "0.3.8", 3858 + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", 3859 + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", 3860 + "dev": true, 3861 + "license": "MIT", 3862 + "dependencies": { 3863 + "@jridgewell/trace-mapping": "^0.3.31", 3864 + "estree-walker": "^3.0.3", 3865 + "js-tokens": "^9.0.1" 3866 + } 3867 + }, 3868 + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { 3869 + "version": "3.0.3", 3870 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 3871 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 3872 + "dev": true, 3873 + "license": "MIT", 3874 + "dependencies": { 3875 + "@types/estree": "^1.0.0" 3876 + } 3877 + }, 3878 + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { 3879 + "version": "9.0.1", 3880 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", 3881 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", 3882 + "dev": true, 3883 + "license": "MIT" 3884 + }, 4862 3885 "node_modules/available-typed-arrays": { 4863 3886 "version": "1.0.7", 4864 3887 "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", ··· 4889 3912 } 4890 3913 } 4891 3914 }, 4892 - "node_modules/babel-jest": { 4893 - "version": "30.2.0", 4894 - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", 4895 - "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", 4896 - "dev": true, 4897 - "license": "MIT", 4898 - "dependencies": { 4899 - "@jest/transform": "30.2.0", 4900 - "@types/babel__core": "^7.20.5", 4901 - "babel-plugin-istanbul": "^7.0.1", 4902 - "babel-preset-jest": "30.2.0", 4903 - "chalk": "^4.1.2", 4904 - "graceful-fs": "^4.2.11", 4905 - "slash": "^3.0.0" 4906 - }, 4907 - "engines": { 4908 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 4909 - }, 4910 - "peerDependencies": { 4911 - "@babel/core": "^7.11.0 || ^8.0.0-0" 4912 - } 4913 - }, 4914 - "node_modules/babel-plugin-istanbul": { 4915 - "version": "7.0.1", 4916 - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", 4917 - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", 4918 - "dev": true, 4919 - "license": "BSD-3-Clause", 4920 - "workspaces": [ 4921 - "test/babel-8" 4922 - ], 4923 - "dependencies": { 4924 - "@babel/helper-plugin-utils": "^7.0.0", 4925 - "@istanbuljs/load-nyc-config": "^1.0.0", 4926 - "@istanbuljs/schema": "^0.1.3", 4927 - "istanbul-lib-instrument": "^6.0.2", 4928 - "test-exclude": "^6.0.0" 4929 - }, 4930 - "engines": { 4931 - "node": ">=12" 4932 - } 4933 - }, 4934 - "node_modules/babel-plugin-jest-hoist": { 4935 - "version": "30.2.0", 4936 - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", 4937 - "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", 4938 - "dev": true, 4939 - "license": "MIT", 4940 - "dependencies": { 4941 - "@types/babel__core": "^7.20.5" 4942 - }, 4943 - "engines": { 4944 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 4945 - } 4946 - }, 4947 3915 "node_modules/babel-plugin-jsx-dom-expressions": { 4948 3916 "version": "0.40.3", 4949 3917 "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz", ··· 4974 3942 "node": ">=6.9.0" 4975 3943 } 4976 3944 }, 4977 - "node_modules/babel-preset-current-node-syntax": { 4978 - "version": "1.2.0", 4979 - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", 4980 - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", 4981 - "dev": true, 4982 - "license": "MIT", 4983 - "dependencies": { 4984 - "@babel/plugin-syntax-async-generators": "^7.8.4", 4985 - "@babel/plugin-syntax-bigint": "^7.8.3", 4986 - "@babel/plugin-syntax-class-properties": "^7.12.13", 4987 - "@babel/plugin-syntax-class-static-block": "^7.14.5", 4988 - "@babel/plugin-syntax-import-attributes": "^7.24.7", 4989 - "@babel/plugin-syntax-import-meta": "^7.10.4", 4990 - "@babel/plugin-syntax-json-strings": "^7.8.3", 4991 - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", 4992 - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", 4993 - "@babel/plugin-syntax-numeric-separator": "^7.10.4", 4994 - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", 4995 - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", 4996 - "@babel/plugin-syntax-optional-chaining": "^7.8.3", 4997 - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", 4998 - "@babel/plugin-syntax-top-level-await": "^7.14.5" 4999 - }, 5000 - "peerDependencies": { 5001 - "@babel/core": "^7.0.0 || ^8.0.0-0" 5002 - } 5003 - }, 5004 - "node_modules/babel-preset-jest": { 5005 - "version": "30.2.0", 5006 - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", 5007 - "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", 5008 - "dev": true, 5009 - "license": "MIT", 5010 - "dependencies": { 5011 - "babel-plugin-jest-hoist": "30.2.0", 5012 - "babel-preset-current-node-syntax": "^1.2.0" 5013 - }, 5014 - "engines": { 5015 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 5016 - }, 5017 - "peerDependencies": { 5018 - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" 5019 - } 5020 - }, 5021 3945 "node_modules/babel-preset-solid": { 5022 3946 "version": "1.9.10", 5023 3947 "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz", ··· 5245 4169 "node": ">=12.20.0" 5246 4170 } 5247 4171 }, 5248 - "node_modules/better-sqlite3": { 5249 - "version": "12.4.1", 5250 - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", 5251 - "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", 5252 - "hasInstallScript": true, 5253 - "license": "MIT", 5254 - "dependencies": { 5255 - "bindings": "^1.5.0", 5256 - "prebuild-install": "^7.1.1" 5257 - }, 5258 - "engines": { 5259 - "node": "20.x || 22.x || 23.x || 24.x" 5260 - } 5261 - }, 5262 4172 "node_modules/binary-extensions": { 5263 4173 "version": "2.3.0", 5264 4174 "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", ··· 5276 4186 "version": "1.5.0", 5277 4187 "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 5278 4188 "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 4189 + "dev": true, 5279 4190 "license": "MIT", 4191 + "optional": true, 5280 4192 "dependencies": { 5281 4193 "file-uri-to-path": "1.0.0" 5282 4194 } ··· 5771 4683 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 5772 4684 } 5773 4685 }, 5774 - "node_modules/bs-logger": { 5775 - "version": "0.2.6", 5776 - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", 5777 - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", 5778 - "dev": true, 5779 - "license": "MIT", 5780 - "dependencies": { 5781 - "fast-json-stable-stringify": "2.x" 5782 - }, 5783 - "engines": { 5784 - "node": ">= 6" 5785 - } 5786 - }, 5787 - "node_modules/bser": { 5788 - "version": "2.1.1", 5789 - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", 5790 - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", 5791 - "dev": true, 5792 - "license": "Apache-2.0", 5793 - "dependencies": { 5794 - "node-int64": "^0.4.0" 5795 - } 5796 - }, 5797 4686 "node_modules/buffer": { 5798 4687 "version": "6.0.3", 5799 4688 "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", ··· 5817 4706 "base64-js": "^1.3.1", 5818 4707 "ieee754": "^1.2.1" 5819 4708 } 5820 - }, 5821 - "node_modules/buffer-from": { 5822 - "version": "1.1.2", 5823 - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 5824 - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 5825 - "dev": true, 5826 - "license": "MIT" 5827 4709 }, 5828 4710 "node_modules/buffer-xor": { 5829 4711 "version": "1.0.3", ··· 6088 4970 "node": ">=6" 6089 4971 } 6090 4972 }, 6091 - "node_modules/camelcase": { 6092 - "version": "5.3.1", 6093 - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 6094 - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 6095 - "dev": true, 6096 - "license": "MIT", 6097 - "engines": { 6098 - "node": ">=6" 6099 - } 6100 - }, 6101 4973 "node_modules/caniuse-lite": { 6102 4974 "version": "1.0.30001753", 6103 4975 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", ··· 6146 5018 "url": "https://github.com/sponsors/wooorm" 6147 5019 } 6148 5020 }, 5021 + "node_modules/chai": { 5022 + "version": "6.2.1", 5023 + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", 5024 + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", 5025 + "dev": true, 5026 + "license": "MIT", 5027 + "engines": { 5028 + "node": ">=18" 5029 + } 5030 + }, 6149 5031 "node_modules/chalk": { 6150 5032 "version": "4.1.2", 6151 5033 "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", ··· 6163 5045 "url": "https://github.com/chalk/chalk?sponsor=1" 6164 5046 } 6165 5047 }, 6166 - "node_modules/char-regex": { 6167 - "version": "1.0.2", 6168 - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", 6169 - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", 6170 - "dev": true, 6171 - "license": "MIT", 6172 - "engines": { 6173 - "node": ">=10" 6174 - } 6175 - }, 6176 5048 "node_modules/character-entities": { 6177 5049 "version": "2.0.2", 6178 5050 "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", ··· 6271 5143 "block-iterator": "^1.1.1" 6272 5144 } 6273 5145 }, 6274 - "node_modules/ci-info": { 6275 - "version": "4.3.1", 6276 - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", 6277 - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", 6278 - "dev": true, 6279 - "funding": [ 6280 - { 6281 - "type": "github", 6282 - "url": "https://github.com/sponsors/sibiraj-s" 6283 - } 6284 - ], 6285 - "license": "MIT", 6286 - "engines": { 6287 - "node": ">=8" 6288 - } 6289 - }, 6290 5146 "node_modules/cipher-base": { 6291 5147 "version": "1.0.7", 6292 5148 "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", ··· 6301 5157 "engines": { 6302 5158 "node": ">= 0.10" 6303 5159 } 6304 - }, 6305 - "node_modules/cjs-module-lexer": { 6306 - "version": "2.1.0", 6307 - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", 6308 - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", 6309 - "dev": true, 6310 - "license": "MIT" 6311 5160 }, 6312 5161 "node_modules/classic-level": { 6313 5162 "version": "3.0.0", ··· 6336 5185 "node": ">=6" 6337 5186 } 6338 5187 }, 6339 - "node_modules/cliui": { 6340 - "version": "8.0.1", 6341 - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 6342 - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 6343 - "dev": true, 6344 - "license": "ISC", 6345 - "dependencies": { 6346 - "string-width": "^4.2.0", 6347 - "strip-ansi": "^6.0.1", 6348 - "wrap-ansi": "^7.0.0" 6349 - }, 6350 - "engines": { 6351 - "node": ">=12" 6352 - } 6353 - }, 6354 - "node_modules/cliui/node_modules/emoji-regex": { 6355 - "version": "8.0.0", 6356 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 6357 - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 6358 - "dev": true, 6359 - "license": "MIT" 6360 - }, 6361 - "node_modules/cliui/node_modules/string-width": { 6362 - "version": "4.2.3", 6363 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 6364 - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 6365 - "dev": true, 6366 - "license": "MIT", 6367 - "dependencies": { 6368 - "emoji-regex": "^8.0.0", 6369 - "is-fullwidth-code-point": "^3.0.0", 6370 - "strip-ansi": "^6.0.1" 6371 - }, 6372 - "engines": { 6373 - "node": ">=8" 6374 - } 6375 - }, 6376 - "node_modules/cliui/node_modules/strip-ansi": { 6377 - "version": "6.0.1", 6378 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 6379 - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 6380 - "dev": true, 6381 - "license": "MIT", 6382 - "dependencies": { 6383 - "ansi-regex": "^5.0.1" 6384 - }, 6385 - "engines": { 6386 - "node": ">=8" 6387 - } 6388 - }, 6389 - "node_modules/cliui/node_modules/wrap-ansi": { 6390 - "version": "7.0.0", 6391 - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 6392 - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 6393 - "dev": true, 6394 - "license": "MIT", 6395 - "dependencies": { 6396 - "ansi-styles": "^4.0.0", 6397 - "string-width": "^4.1.0", 6398 - "strip-ansi": "^6.0.0" 6399 - }, 6400 - "engines": { 6401 - "node": ">=10" 6402 - }, 6403 - "funding": { 6404 - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 6405 - } 6406 - }, 6407 5188 "node_modules/clsx": { 6408 5189 "version": "2.1.1", 6409 5190 "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", ··· 6413 5194 "node": ">=6" 6414 5195 } 6415 5196 }, 6416 - "node_modules/co": { 6417 - "version": "4.6.0", 6418 - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 6419 - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", 6420 - "dev": true, 6421 - "license": "MIT", 6422 - "engines": { 6423 - "iojs": ">= 1.0.0", 6424 - "node": ">= 0.12.0" 6425 - } 6426 - }, 6427 - "node_modules/collect-v8-coverage": { 6428 - "version": "1.0.3", 6429 - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", 6430 - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", 6431 - "dev": true, 6432 - "license": "MIT" 6433 - }, 6434 5197 "node_modules/color-convert": { 6435 5198 "version": "2.0.1", 6436 5199 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", ··· 6876 5639 "url": "https://github.com/sponsors/sindresorhus" 6877 5640 } 6878 5641 }, 6879 - "node_modules/dedent": { 6880 - "version": "1.7.0", 6881 - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", 6882 - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", 6883 - "dev": true, 6884 - "license": "MIT", 6885 - "peerDependencies": { 6886 - "babel-plugin-macros": "^3.1.0" 6887 - }, 6888 - "peerDependenciesMeta": { 6889 - "babel-plugin-macros": { 6890 - "optional": true 6891 - } 6892 - } 6893 - }, 6894 5642 "node_modules/deep-extend": { 6895 5643 "version": "0.6.0", 6896 5644 "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", ··· 6906 5654 "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 6907 5655 "dev": true, 6908 5656 "license": "MIT" 6909 - }, 6910 - "node_modules/deepmerge": { 6911 - "version": "4.3.1", 6912 - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 6913 - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 6914 - "dev": true, 6915 - "license": "MIT", 6916 - "engines": { 6917 - "node": ">=0.10.0" 6918 - } 6919 5657 }, 6920 5658 "node_modules/default-gateway": { 6921 5659 "version": "7.2.2", ··· 7128 5866 "node": ">=8" 7129 5867 } 7130 5868 }, 7131 - "node_modules/detect-newline": { 7132 - "version": "3.1.0", 7133 - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", 7134 - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", 7135 - "dev": true, 7136 - "license": "MIT", 7137 - "engines": { 7138 - "node": ">=8" 7139 - } 7140 - }, 7141 5869 "node_modules/devlop": { 7142 5870 "version": "1.1.0", 7143 5871 "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", ··· 7157 5885 "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz", 7158 5886 "integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==", 7159 5887 "license": "Apache-2.0" 7160 - }, 7161 - "node_modules/diff-sequences": { 7162 - "version": "29.6.3", 7163 - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", 7164 - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", 7165 - "dev": true, 7166 - "license": "MIT", 7167 - "engines": { 7168 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 7169 - } 7170 5888 }, 7171 5889 "node_modules/diffie-hellman": { 7172 5890 "version": "5.0.3", ··· 7291 6009 "node": ">= 0.4" 7292 6010 } 7293 6011 }, 7294 - "node_modules/eastasianwidth": { 7295 - "version": "0.2.0", 7296 - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 7297 - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 7298 - "dev": true, 7299 - "license": "MIT" 7300 - }, 7301 6012 "node_modules/ee-first": { 7302 6013 "version": "1.1.1", 7303 6014 "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", ··· 7334 6045 "dev": true, 7335 6046 "license": "MIT" 7336 6047 }, 7337 - "node_modules/emittery": { 7338 - "version": "0.13.1", 7339 - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", 7340 - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", 7341 - "dev": true, 7342 - "license": "MIT", 7343 - "engines": { 7344 - "node": ">=12" 7345 - }, 7346 - "funding": { 7347 - "url": "https://github.com/sindresorhus/emittery?sponsor=1" 7348 - } 7349 - }, 7350 - "node_modules/emoji-regex": { 7351 - "version": "9.2.2", 7352 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 7353 - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 7354 - "dev": true, 7355 - "license": "MIT" 7356 - }, 7357 6048 "node_modules/encodeurl": { 7358 6049 "version": "2.0.0", 7359 6050 "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", ··· 7427 6118 "license": "MIT", 7428 6119 "optional": true 7429 6120 }, 7430 - "node_modules/error-ex": { 7431 - "version": "1.3.4", 7432 - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", 7433 - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", 7434 - "dev": true, 7435 - "license": "MIT", 7436 - "dependencies": { 7437 - "is-arrayish": "^0.2.1" 7438 - } 7439 - }, 7440 6121 "node_modules/es-define-property": { 7441 6122 "version": "1.0.1", 7442 6123 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", ··· 7454 6135 "engines": { 7455 6136 "node": ">= 0.4" 7456 6137 } 6138 + }, 6139 + "node_modules/es-module-lexer": { 6140 + "version": "1.7.0", 6141 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 6142 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 6143 + "dev": true, 6144 + "license": "MIT" 7457 6145 }, 7458 6146 "node_modules/es-object-atoms": { 7459 6147 "version": "1.1.1", ··· 7791 6479 "url": "https://opencollective.com/eslint" 7792 6480 } 7793 6481 }, 7794 - "node_modules/esprima": { 7795 - "version": "4.0.1", 7796 - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 7797 - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 7798 - "dev": true, 7799 - "license": "BSD-2-Clause", 7800 - "bin": { 7801 - "esparse": "bin/esparse.js", 7802 - "esvalidate": "bin/esvalidate.js" 7803 - }, 7804 - "engines": { 7805 - "node": ">=4" 7806 - } 7807 - }, 7808 6482 "node_modules/esquery": { 7809 6483 "version": "1.6.0", 7810 6484 "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", ··· 7907 6581 "safe-buffer": "^5.1.1" 7908 6582 } 7909 6583 }, 7910 - "node_modules/execa": { 7911 - "version": "5.1.1", 7912 - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", 7913 - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", 7914 - "dev": true, 7915 - "license": "MIT", 7916 - "dependencies": { 7917 - "cross-spawn": "^7.0.3", 7918 - "get-stream": "^6.0.0", 7919 - "human-signals": "^2.1.0", 7920 - "is-stream": "^2.0.0", 7921 - "merge-stream": "^2.0.0", 7922 - "npm-run-path": "^4.0.1", 7923 - "onetime": "^5.1.2", 7924 - "signal-exit": "^3.0.3", 7925 - "strip-final-newline": "^2.0.0" 7926 - }, 7927 - "engines": { 7928 - "node": ">=10" 7929 - }, 7930 - "funding": { 7931 - "url": "https://github.com/sindresorhus/execa?sponsor=1" 7932 - } 7933 - }, 7934 - "node_modules/execa/node_modules/signal-exit": { 7935 - "version": "3.0.7", 7936 - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 7937 - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 7938 - "dev": true, 7939 - "license": "ISC" 7940 - }, 7941 - "node_modules/exit-x": { 7942 - "version": "0.2.2", 7943 - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", 7944 - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", 7945 - "dev": true, 7946 - "license": "MIT", 7947 - "engines": { 7948 - "node": ">= 0.8.0" 7949 - } 7950 - }, 7951 6584 "node_modules/expand-template": { 7952 6585 "version": "2.0.3", 7953 6586 "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", ··· 7957 6590 "node": ">=6" 7958 6591 } 7959 6592 }, 7960 - "node_modules/expect": { 7961 - "version": "30.2.0", 7962 - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", 7963 - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", 6593 + "node_modules/expect-type": { 6594 + "version": "1.2.2", 6595 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", 6596 + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", 7964 6597 "dev": true, 7965 - "license": "MIT", 7966 - "dependencies": { 7967 - "@jest/expect-utils": "30.2.0", 7968 - "@jest/get-type": "30.1.0", 7969 - "jest-matcher-utils": "30.2.0", 7970 - "jest-message-util": "30.2.0", 7971 - "jest-mock": "30.2.0", 7972 - "jest-util": "30.2.0" 7973 - }, 6598 + "license": "Apache-2.0", 7974 6599 "engines": { 7975 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 6600 + "node": ">=12.0.0" 7976 6601 } 7977 6602 }, 7978 6603 "node_modules/express": { ··· 8129 6754 "url": "https://github.com/sponsors/wooorm" 8130 6755 } 8131 6756 }, 8132 - "node_modules/fb-watchman": { 8133 - "version": "2.0.2", 8134 - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", 8135 - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", 8136 - "dev": true, 8137 - "license": "Apache-2.0", 8138 - "dependencies": { 8139 - "bser": "2.1.1" 8140 - } 8141 - }, 8142 6757 "node_modules/feedsmith": { 8143 6758 "version": "2.4.0", 8144 6759 "resolved": "https://registry.npmjs.org/feedsmith/-/feedsmith-2.4.0.tgz", ··· 8189 6804 "version": "1.0.0", 8190 6805 "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 8191 6806 "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 8192 - "license": "MIT" 6807 + "dev": true, 6808 + "license": "MIT", 6809 + "optional": true 8193 6810 }, 8194 6811 "node_modules/filename-reserved-regex": { 8195 6812 "version": "3.0.0", ··· 8287 6904 "url": "https://github.com/sponsors/ljharb" 8288 6905 } 8289 6906 }, 8290 - "node_modules/foreground-child": { 8291 - "version": "3.3.1", 8292 - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", 8293 - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", 8294 - "dev": true, 8295 - "license": "ISC", 8296 - "dependencies": { 8297 - "cross-spawn": "^7.0.6", 8298 - "signal-exit": "^4.0.1" 8299 - }, 8300 - "engines": { 8301 - "node": ">=14" 8302 - }, 8303 - "funding": { 8304 - "url": "https://github.com/sponsors/isaacs" 8305 - } 8306 - }, 8307 6907 "node_modules/format": { 8308 6908 "version": "0.2.2", 8309 6909 "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", ··· 8438 7038 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 8439 7039 "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 8440 7040 "dev": true, 8441 - "license": "ISC" 7041 + "license": "ISC", 7042 + "optional": true 8442 7043 }, 8443 7044 "node_modules/fsa-chunk-store": { 8444 7045 "version": "1.3.0", ··· 8560 7161 "node": ">=6.9.0" 8561 7162 } 8562 7163 }, 8563 - "node_modules/get-caller-file": { 8564 - "version": "2.0.5", 8565 - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 8566 - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 8567 - "dev": true, 8568 - "license": "ISC", 8569 - "engines": { 8570 - "node": "6.* || 8.* || >= 10.*" 8571 - } 8572 - }, 8573 7164 "node_modules/get-intrinsic": { 8574 7165 "version": "1.3.0", 8575 7166 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", ··· 8594 7185 "url": "https://github.com/sponsors/ljharb" 8595 7186 } 8596 7187 }, 8597 - "node_modules/get-package-type": { 8598 - "version": "0.1.0", 8599 - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", 8600 - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", 8601 - "dev": true, 8602 - "license": "MIT", 8603 - "engines": { 8604 - "node": ">=8.0.0" 8605 - } 8606 - }, 8607 7188 "node_modules/get-proto": { 8608 7189 "version": "1.0.1", 8609 7190 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", ··· 8659 7240 "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 8660 7241 "license": "MIT" 8661 7242 }, 8662 - "node_modules/glob": { 8663 - "version": "10.4.5", 8664 - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 8665 - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 8666 - "dev": true, 8667 - "license": "ISC", 8668 - "dependencies": { 8669 - "foreground-child": "^3.1.0", 8670 - "jackspeak": "^3.1.2", 8671 - "minimatch": "^9.0.4", 8672 - "minipass": "^7.1.2", 8673 - "package-json-from-dist": "^1.0.0", 8674 - "path-scurry": "^1.11.1" 8675 - }, 8676 - "bin": { 8677 - "glob": "dist/esm/bin.mjs" 8678 - }, 8679 - "funding": { 8680 - "url": "https://github.com/sponsors/isaacs" 8681 - } 8682 - }, 8683 7243 "node_modules/glob-parent": { 8684 7244 "version": "6.0.2", 8685 7245 "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", ··· 8710 7270 "tslib": "2" 8711 7271 } 8712 7272 }, 8713 - "node_modules/glob-to-regexp": { 8714 - "version": "0.4.1", 8715 - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 8716 - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 8717 - "dev": true, 8718 - "license": "BSD-2-Clause" 8719 - }, 8720 7273 "node_modules/globals": { 8721 7274 "version": "16.5.0", 8722 7275 "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", ··· 8755 7308 "dev": true, 8756 7309 "license": "MIT" 8757 7310 }, 8758 - "node_modules/handlebars": { 8759 - "version": "4.7.8", 8760 - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", 8761 - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", 8762 - "dev": true, 8763 - "license": "MIT", 8764 - "dependencies": { 8765 - "minimist": "^1.2.5", 8766 - "neo-async": "^2.6.2", 8767 - "source-map": "^0.6.1", 8768 - "wordwrap": "^1.0.0" 8769 - }, 8770 - "bin": { 8771 - "handlebars": "bin/handlebars" 8772 - }, 8773 - "engines": { 8774 - "node": ">=0.4.7" 8775 - }, 8776 - "optionalDependencies": { 8777 - "uglify-js": "^3.1.4" 8778 - } 8779 - }, 8780 7311 "node_modules/harmony-reflect": { 8781 7312 "version": "1.6.2", 8782 7313 "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", ··· 9043 7574 "node": ">= 14" 9044 7575 } 9045 7576 }, 9046 - "node_modules/human-signals": { 9047 - "version": "2.1.0", 9048 - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", 9049 - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", 9050 - "dev": true, 9051 - "license": "Apache-2.0", 9052 - "engines": { 9053 - "node": ">=10.17.0" 9054 - } 9055 - }, 9056 7577 "node_modules/humanize-ms": { 9057 7578 "version": "1.2.1", 9058 7579 "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", ··· 9176 7697 "url": "https://github.com/sponsors/sindresorhus" 9177 7698 } 9178 7699 }, 9179 - "node_modules/import-local": { 9180 - "version": "3.2.0", 9181 - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", 9182 - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", 9183 - "dev": true, 9184 - "license": "MIT", 9185 - "dependencies": { 9186 - "pkg-dir": "^4.2.0", 9187 - "resolve-cwd": "^3.0.0" 9188 - }, 9189 - "bin": { 9190 - "import-local-fixture": "fixtures/cli.js" 9191 - }, 9192 - "engines": { 9193 - "node": ">=8" 9194 - }, 9195 - "funding": { 9196 - "url": "https://github.com/sponsors/sindresorhus" 9197 - } 9198 - }, 9199 7700 "node_modules/imurmurhash": { 9200 7701 "version": "0.1.4", 9201 7702 "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", ··· 9249 7750 "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 9250 7751 "dev": true, 9251 7752 "license": "ISC", 7753 + "optional": true, 9252 7754 "dependencies": { 9253 7755 "once": "^1.3.0", 9254 7756 "wrappy": "1" ··· 9323 7825 "url": "https://github.com/sponsors/ljharb" 9324 7826 } 9325 7827 }, 9326 - "node_modules/is-arrayish": { 9327 - "version": "0.2.1", 9328 - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 9329 - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", 9330 - "dev": true, 9331 - "license": "MIT" 9332 - }, 9333 7828 "node_modules/is-binary-path": { 9334 7829 "version": "2.1.0", 9335 7830 "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", ··· 9417 7912 "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 9418 7913 "dev": true, 9419 7914 "license": "MIT", 7915 + "optional": true, 9420 7916 "engines": { 9421 7917 "node": ">=8" 9422 7918 } 9423 7919 }, 9424 - "node_modules/is-generator-fn": { 9425 - "version": "2.1.0", 9426 - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", 9427 - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", 9428 - "dev": true, 9429 - "license": "MIT", 9430 - "engines": { 9431 - "node": ">=6" 9432 - } 9433 - }, 9434 7920 "node_modules/is-generator-function": { 9435 7921 "version": "1.1.2", 9436 7922 "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", ··· 9544 8030 "url": "https://github.com/sponsors/ljharb" 9545 8031 } 9546 8032 }, 9547 - "node_modules/is-stream": { 9548 - "version": "2.0.1", 9549 - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 9550 - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", 9551 - "dev": true, 9552 - "license": "MIT", 9553 - "engines": { 9554 - "node": ">=8" 9555 - }, 9556 - "funding": { 9557 - "url": "https://github.com/sponsors/sindresorhus" 9558 - } 9559 - }, 9560 8033 "node_modules/is-typed-array": { 9561 8034 "version": "1.1.15", 9562 8035 "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", ··· 9628 8101 "node": ">=8" 9629 8102 } 9630 8103 }, 9631 - "node_modules/istanbul-lib-instrument": { 9632 - "version": "6.0.3", 9633 - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", 9634 - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", 9635 - "dev": true, 9636 - "license": "BSD-3-Clause", 9637 - "dependencies": { 9638 - "@babel/core": "^7.23.9", 9639 - "@babel/parser": "^7.23.9", 9640 - "@istanbuljs/schema": "^0.1.3", 9641 - "istanbul-lib-coverage": "^3.2.0", 9642 - "semver": "^7.5.4" 9643 - }, 9644 - "engines": { 9645 - "node": ">=10" 9646 - } 9647 - }, 9648 - "node_modules/istanbul-lib-instrument/node_modules/semver": { 9649 - "version": "7.7.3", 9650 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 9651 - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 9652 - "dev": true, 9653 - "license": "ISC", 9654 - "bin": { 9655 - "semver": "bin/semver.js" 9656 - }, 9657 - "engines": { 9658 - "node": ">=10" 9659 - } 9660 - }, 9661 8104 "node_modules/istanbul-lib-report": { 9662 8105 "version": "3.0.1", 9663 8106 "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", ··· 9702 8145 "node": ">=8" 9703 8146 } 9704 8147 }, 9705 - "node_modules/jackspeak": { 9706 - "version": "3.4.3", 9707 - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 9708 - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 9709 - "dev": true, 9710 - "license": "BlueOak-1.0.0", 9711 - "dependencies": { 9712 - "@isaacs/cliui": "^8.0.2" 9713 - }, 9714 - "funding": { 9715 - "url": "https://github.com/sponsors/isaacs" 9716 - }, 9717 - "optionalDependencies": { 9718 - "@pkgjs/parseargs": "^0.11.0" 9719 - } 9720 - }, 9721 8148 "node_modules/javascript-natural-sort": { 9722 8149 "version": "0.7.1", 9723 8150 "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", ··· 9725 8152 "dev": true, 9726 8153 "license": "MIT" 9727 8154 }, 9728 - "node_modules/jest": { 9729 - "version": "30.2.0", 9730 - "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", 9731 - "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", 9732 - "dev": true, 9733 - "license": "MIT", 9734 - "peer": true, 9735 - "dependencies": { 9736 - "@jest/core": "30.2.0", 9737 - "@jest/types": "30.2.0", 9738 - "import-local": "^3.2.0", 9739 - "jest-cli": "30.2.0" 9740 - }, 9741 - "bin": { 9742 - "jest": "bin/jest.js" 9743 - }, 9744 - "engines": { 9745 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9746 - }, 9747 - "peerDependencies": { 9748 - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" 9749 - }, 9750 - "peerDependenciesMeta": { 9751 - "node-notifier": { 9752 - "optional": true 9753 - } 9754 - } 9755 - }, 9756 - "node_modules/jest-changed-files": { 9757 - "version": "30.2.0", 9758 - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", 9759 - "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", 9760 - "dev": true, 9761 - "license": "MIT", 9762 - "dependencies": { 9763 - "execa": "^5.1.1", 9764 - "jest-util": "30.2.0", 9765 - "p-limit": "^3.1.0" 9766 - }, 9767 - "engines": { 9768 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9769 - } 9770 - }, 9771 - "node_modules/jest-circus": { 9772 - "version": "30.2.0", 9773 - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", 9774 - "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", 9775 - "dev": true, 9776 - "license": "MIT", 9777 - "dependencies": { 9778 - "@jest/environment": "30.2.0", 9779 - "@jest/expect": "30.2.0", 9780 - "@jest/test-result": "30.2.0", 9781 - "@jest/types": "30.2.0", 9782 - "@types/node": "*", 9783 - "chalk": "^4.1.2", 9784 - "co": "^4.6.0", 9785 - "dedent": "^1.6.0", 9786 - "is-generator-fn": "^2.1.0", 9787 - "jest-each": "30.2.0", 9788 - "jest-matcher-utils": "30.2.0", 9789 - "jest-message-util": "30.2.0", 9790 - "jest-runtime": "30.2.0", 9791 - "jest-snapshot": "30.2.0", 9792 - "jest-util": "30.2.0", 9793 - "p-limit": "^3.1.0", 9794 - "pretty-format": "30.2.0", 9795 - "pure-rand": "^7.0.0", 9796 - "slash": "^3.0.0", 9797 - "stack-utils": "^2.0.6" 9798 - }, 9799 - "engines": { 9800 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9801 - } 9802 - }, 9803 - "node_modules/jest-circus/node_modules/ansi-styles": { 9804 - "version": "5.2.0", 9805 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 9806 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 9807 - "dev": true, 9808 - "license": "MIT", 9809 - "engines": { 9810 - "node": ">=10" 9811 - }, 9812 - "funding": { 9813 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 9814 - } 9815 - }, 9816 - "node_modules/jest-circus/node_modules/pretty-format": { 9817 - "version": "30.2.0", 9818 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 9819 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 9820 - "dev": true, 9821 - "license": "MIT", 9822 - "dependencies": { 9823 - "@jest/schemas": "30.0.5", 9824 - "ansi-styles": "^5.2.0", 9825 - "react-is": "^18.3.1" 9826 - }, 9827 - "engines": { 9828 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9829 - } 9830 - }, 9831 - "node_modules/jest-circus/node_modules/react-is": { 9832 - "version": "18.3.1", 9833 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 9834 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 9835 - "dev": true, 9836 - "license": "MIT" 9837 - }, 9838 - "node_modules/jest-cli": { 9839 - "version": "30.2.0", 9840 - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", 9841 - "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", 9842 - "dev": true, 9843 - "license": "MIT", 9844 - "dependencies": { 9845 - "@jest/core": "30.2.0", 9846 - "@jest/test-result": "30.2.0", 9847 - "@jest/types": "30.2.0", 9848 - "chalk": "^4.1.2", 9849 - "exit-x": "^0.2.2", 9850 - "import-local": "^3.2.0", 9851 - "jest-config": "30.2.0", 9852 - "jest-util": "30.2.0", 9853 - "jest-validate": "30.2.0", 9854 - "yargs": "^17.7.2" 9855 - }, 9856 - "bin": { 9857 - "jest": "bin/jest.js" 9858 - }, 9859 - "engines": { 9860 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9861 - }, 9862 - "peerDependencies": { 9863 - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" 9864 - }, 9865 - "peerDependenciesMeta": { 9866 - "node-notifier": { 9867 - "optional": true 9868 - } 9869 - } 9870 - }, 9871 - "node_modules/jest-config": { 9872 - "version": "30.2.0", 9873 - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", 9874 - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", 9875 - "dev": true, 9876 - "license": "MIT", 9877 - "dependencies": { 9878 - "@babel/core": "^7.27.4", 9879 - "@jest/get-type": "30.1.0", 9880 - "@jest/pattern": "30.0.1", 9881 - "@jest/test-sequencer": "30.2.0", 9882 - "@jest/types": "30.2.0", 9883 - "babel-jest": "30.2.0", 9884 - "chalk": "^4.1.2", 9885 - "ci-info": "^4.2.0", 9886 - "deepmerge": "^4.3.1", 9887 - "glob": "^10.3.10", 9888 - "graceful-fs": "^4.2.11", 9889 - "jest-circus": "30.2.0", 9890 - "jest-docblock": "30.2.0", 9891 - "jest-environment-node": "30.2.0", 9892 - "jest-regex-util": "30.0.1", 9893 - "jest-resolve": "30.2.0", 9894 - "jest-runner": "30.2.0", 9895 - "jest-util": "30.2.0", 9896 - "jest-validate": "30.2.0", 9897 - "micromatch": "^4.0.8", 9898 - "parse-json": "^5.2.0", 9899 - "pretty-format": "30.2.0", 9900 - "slash": "^3.0.0", 9901 - "strip-json-comments": "^3.1.1" 9902 - }, 9903 - "engines": { 9904 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9905 - }, 9906 - "peerDependencies": { 9907 - "@types/node": "*", 9908 - "esbuild-register": ">=3.4.0", 9909 - "ts-node": ">=9.0.0" 9910 - }, 9911 - "peerDependenciesMeta": { 9912 - "@types/node": { 9913 - "optional": true 9914 - }, 9915 - "esbuild-register": { 9916 - "optional": true 9917 - }, 9918 - "ts-node": { 9919 - "optional": true 9920 - } 9921 - } 9922 - }, 9923 - "node_modules/jest-config/node_modules/ansi-styles": { 9924 - "version": "5.2.0", 9925 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 9926 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 9927 - "dev": true, 9928 - "license": "MIT", 9929 - "engines": { 9930 - "node": ">=10" 9931 - }, 9932 - "funding": { 9933 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 9934 - } 9935 - }, 9936 - "node_modules/jest-config/node_modules/pretty-format": { 9937 - "version": "30.2.0", 9938 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 9939 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 9940 - "dev": true, 9941 - "license": "MIT", 9942 - "dependencies": { 9943 - "@jest/schemas": "30.0.5", 9944 - "ansi-styles": "^5.2.0", 9945 - "react-is": "^18.3.1" 9946 - }, 9947 - "engines": { 9948 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9949 - } 9950 - }, 9951 - "node_modules/jest-config/node_modules/react-is": { 9952 - "version": "18.3.1", 9953 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 9954 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 9955 - "dev": true, 9956 - "license": "MIT" 9957 - }, 9958 - "node_modules/jest-diff": { 9959 - "version": "30.2.0", 9960 - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", 9961 - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", 9962 - "dev": true, 9963 - "license": "MIT", 9964 - "dependencies": { 9965 - "@jest/diff-sequences": "30.0.1", 9966 - "@jest/get-type": "30.1.0", 9967 - "chalk": "^4.1.2", 9968 - "pretty-format": "30.2.0" 9969 - }, 9970 - "engines": { 9971 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 9972 - } 9973 - }, 9974 - "node_modules/jest-diff/node_modules/ansi-styles": { 9975 - "version": "5.2.0", 9976 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 9977 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 9978 - "dev": true, 9979 - "license": "MIT", 9980 - "engines": { 9981 - "node": ">=10" 9982 - }, 9983 - "funding": { 9984 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 9985 - } 9986 - }, 9987 - "node_modules/jest-diff/node_modules/pretty-format": { 9988 - "version": "30.2.0", 9989 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 9990 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 9991 - "dev": true, 9992 - "license": "MIT", 9993 - "dependencies": { 9994 - "@jest/schemas": "30.0.5", 9995 - "ansi-styles": "^5.2.0", 9996 - "react-is": "^18.3.1" 9997 - }, 9998 - "engines": { 9999 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10000 - } 10001 - }, 10002 - "node_modules/jest-diff/node_modules/react-is": { 10003 - "version": "18.3.1", 10004 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10005 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10006 - "dev": true, 10007 - "license": "MIT" 10008 - }, 10009 - "node_modules/jest-docblock": { 10010 - "version": "30.2.0", 10011 - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", 10012 - "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", 10013 - "dev": true, 10014 - "license": "MIT", 10015 - "dependencies": { 10016 - "detect-newline": "^3.1.0" 10017 - }, 10018 - "engines": { 10019 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10020 - } 10021 - }, 10022 - "node_modules/jest-each": { 10023 - "version": "30.2.0", 10024 - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", 10025 - "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", 10026 - "dev": true, 10027 - "license": "MIT", 10028 - "dependencies": { 10029 - "@jest/get-type": "30.1.0", 10030 - "@jest/types": "30.2.0", 10031 - "chalk": "^4.1.2", 10032 - "jest-util": "30.2.0", 10033 - "pretty-format": "30.2.0" 10034 - }, 10035 - "engines": { 10036 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10037 - } 10038 - }, 10039 - "node_modules/jest-each/node_modules/ansi-styles": { 10040 - "version": "5.2.0", 10041 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10042 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10043 - "dev": true, 10044 - "license": "MIT", 10045 - "engines": { 10046 - "node": ">=10" 10047 - }, 10048 - "funding": { 10049 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10050 - } 10051 - }, 10052 - "node_modules/jest-each/node_modules/pretty-format": { 10053 - "version": "30.2.0", 10054 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 10055 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 10056 - "dev": true, 10057 - "license": "MIT", 10058 - "dependencies": { 10059 - "@jest/schemas": "30.0.5", 10060 - "ansi-styles": "^5.2.0", 10061 - "react-is": "^18.3.1" 10062 - }, 10063 - "engines": { 10064 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10065 - } 10066 - }, 10067 - "node_modules/jest-each/node_modules/react-is": { 10068 - "version": "18.3.1", 10069 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10070 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10071 - "dev": true, 10072 - "license": "MIT" 10073 - }, 10074 - "node_modules/jest-environment-jsdom": { 10075 - "version": "30.2.0", 10076 - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", 10077 - "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", 10078 - "dev": true, 10079 - "license": "MIT", 10080 - "peer": true, 10081 - "dependencies": { 10082 - "@jest/environment": "30.2.0", 10083 - "@jest/environment-jsdom-abstract": "30.2.0", 10084 - "@types/jsdom": "^21.1.7", 10085 - "@types/node": "*", 10086 - "jsdom": "^26.1.0" 10087 - }, 10088 - "engines": { 10089 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10090 - }, 10091 - "peerDependencies": { 10092 - "canvas": "^3.0.0" 10093 - }, 10094 - "peerDependenciesMeta": { 10095 - "canvas": { 10096 - "optional": true 10097 - } 10098 - } 10099 - }, 10100 - "node_modules/jest-environment-node": { 10101 - "version": "30.2.0", 10102 - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", 10103 - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", 10104 - "dev": true, 10105 - "license": "MIT", 10106 - "dependencies": { 10107 - "@jest/environment": "30.2.0", 10108 - "@jest/fake-timers": "30.2.0", 10109 - "@jest/types": "30.2.0", 10110 - "@types/node": "*", 10111 - "jest-mock": "30.2.0", 10112 - "jest-util": "30.2.0", 10113 - "jest-validate": "30.2.0" 10114 - }, 10115 - "engines": { 10116 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10117 - } 10118 - }, 10119 - "node_modules/jest-fixed-jsdom": { 10120 - "version": "0.0.9", 10121 - "resolved": "https://registry.npmjs.org/jest-fixed-jsdom/-/jest-fixed-jsdom-0.0.9.tgz", 10122 - "integrity": "sha512-KPfqh2+sn5q2B+7LZktwDcwhCpOpUSue8a1I+BcixWLOQoEVyAjAGfH+IYZGoxZsziNojoHGRTC8xRbB1wDD4g==", 10123 - "dev": true, 10124 - "license": "MIT", 10125 - "engines": { 10126 - "node": ">=18.0.0" 10127 - }, 10128 - "peerDependencies": { 10129 - "jest-environment-jsdom": ">=28.0.0" 10130 - } 10131 - }, 10132 - "node_modules/jest-get-type": { 10133 - "version": "29.6.3", 10134 - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", 10135 - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", 10136 - "dev": true, 10137 - "license": "MIT", 10138 - "engines": { 10139 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 10140 - } 10141 - }, 10142 - "node_modules/jest-haste-map": { 10143 - "version": "30.2.0", 10144 - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", 10145 - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", 10146 - "dev": true, 10147 - "license": "MIT", 10148 - "dependencies": { 10149 - "@jest/types": "30.2.0", 10150 - "@types/node": "*", 10151 - "anymatch": "^3.1.3", 10152 - "fb-watchman": "^2.0.2", 10153 - "graceful-fs": "^4.2.11", 10154 - "jest-regex-util": "30.0.1", 10155 - "jest-util": "30.2.0", 10156 - "jest-worker": "30.2.0", 10157 - "micromatch": "^4.0.8", 10158 - "walker": "^1.0.8" 10159 - }, 10160 - "engines": { 10161 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10162 - }, 10163 - "optionalDependencies": { 10164 - "fsevents": "^2.3.3" 10165 - } 10166 - }, 10167 - "node_modules/jest-leak-detector": { 10168 - "version": "30.2.0", 10169 - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", 10170 - "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", 10171 - "dev": true, 10172 - "license": "MIT", 10173 - "dependencies": { 10174 - "@jest/get-type": "30.1.0", 10175 - "pretty-format": "30.2.0" 10176 - }, 10177 - "engines": { 10178 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10179 - } 10180 - }, 10181 - "node_modules/jest-leak-detector/node_modules/ansi-styles": { 10182 - "version": "5.2.0", 10183 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10184 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10185 - "dev": true, 10186 - "license": "MIT", 10187 - "engines": { 10188 - "node": ">=10" 10189 - }, 10190 - "funding": { 10191 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10192 - } 10193 - }, 10194 - "node_modules/jest-leak-detector/node_modules/pretty-format": { 10195 - "version": "30.2.0", 10196 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 10197 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 10198 - "dev": true, 10199 - "license": "MIT", 10200 - "dependencies": { 10201 - "@jest/schemas": "30.0.5", 10202 - "ansi-styles": "^5.2.0", 10203 - "react-is": "^18.3.1" 10204 - }, 10205 - "engines": { 10206 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10207 - } 10208 - }, 10209 - "node_modules/jest-leak-detector/node_modules/react-is": { 10210 - "version": "18.3.1", 10211 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10212 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10213 - "dev": true, 10214 - "license": "MIT" 10215 - }, 10216 - "node_modules/jest-matcher-utils": { 10217 - "version": "30.2.0", 10218 - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", 10219 - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", 10220 - "dev": true, 10221 - "license": "MIT", 10222 - "dependencies": { 10223 - "@jest/get-type": "30.1.0", 10224 - "chalk": "^4.1.2", 10225 - "jest-diff": "30.2.0", 10226 - "pretty-format": "30.2.0" 10227 - }, 10228 - "engines": { 10229 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10230 - } 10231 - }, 10232 - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { 10233 - "version": "5.2.0", 10234 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10235 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10236 - "dev": true, 10237 - "license": "MIT", 10238 - "engines": { 10239 - "node": ">=10" 10240 - }, 10241 - "funding": { 10242 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10243 - } 10244 - }, 10245 - "node_modules/jest-matcher-utils/node_modules/pretty-format": { 10246 - "version": "30.2.0", 10247 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 10248 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 10249 - "dev": true, 10250 - "license": "MIT", 10251 - "dependencies": { 10252 - "@jest/schemas": "30.0.5", 10253 - "ansi-styles": "^5.2.0", 10254 - "react-is": "^18.3.1" 10255 - }, 10256 - "engines": { 10257 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10258 - } 10259 - }, 10260 - "node_modules/jest-matcher-utils/node_modules/react-is": { 10261 - "version": "18.3.1", 10262 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10263 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10264 - "dev": true, 10265 - "license": "MIT" 10266 - }, 10267 - "node_modules/jest-message-util": { 10268 - "version": "30.2.0", 10269 - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", 10270 - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", 10271 - "dev": true, 10272 - "license": "MIT", 10273 - "dependencies": { 10274 - "@babel/code-frame": "^7.27.1", 10275 - "@jest/types": "30.2.0", 10276 - "@types/stack-utils": "^2.0.3", 10277 - "chalk": "^4.1.2", 10278 - "graceful-fs": "^4.2.11", 10279 - "micromatch": "^4.0.8", 10280 - "pretty-format": "30.2.0", 10281 - "slash": "^3.0.0", 10282 - "stack-utils": "^2.0.6" 10283 - }, 10284 - "engines": { 10285 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10286 - } 10287 - }, 10288 - "node_modules/jest-message-util/node_modules/ansi-styles": { 10289 - "version": "5.2.0", 10290 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10291 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10292 - "dev": true, 10293 - "license": "MIT", 10294 - "engines": { 10295 - "node": ">=10" 10296 - }, 10297 - "funding": { 10298 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10299 - } 10300 - }, 10301 - "node_modules/jest-message-util/node_modules/pretty-format": { 10302 - "version": "30.2.0", 10303 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 10304 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 10305 - "dev": true, 10306 - "license": "MIT", 10307 - "dependencies": { 10308 - "@jest/schemas": "30.0.5", 10309 - "ansi-styles": "^5.2.0", 10310 - "react-is": "^18.3.1" 10311 - }, 10312 - "engines": { 10313 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10314 - } 10315 - }, 10316 - "node_modules/jest-message-util/node_modules/react-is": { 10317 - "version": "18.3.1", 10318 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10319 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10320 - "dev": true, 10321 - "license": "MIT" 10322 - }, 10323 - "node_modules/jest-mock": { 10324 - "version": "30.2.0", 10325 - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", 10326 - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", 10327 - "dev": true, 10328 - "license": "MIT", 10329 - "dependencies": { 10330 - "@jest/types": "30.2.0", 10331 - "@types/node": "*", 10332 - "jest-util": "30.2.0" 10333 - }, 10334 - "engines": { 10335 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10336 - } 10337 - }, 10338 - "node_modules/jest-pnp-resolver": { 10339 - "version": "1.2.3", 10340 - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", 10341 - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", 10342 - "dev": true, 10343 - "license": "MIT", 10344 - "engines": { 10345 - "node": ">=6" 10346 - }, 10347 - "peerDependencies": { 10348 - "jest-resolve": "*" 10349 - }, 10350 - "peerDependenciesMeta": { 10351 - "jest-resolve": { 10352 - "optional": true 10353 - } 10354 - } 10355 - }, 10356 - "node_modules/jest-regex-util": { 10357 - "version": "30.0.1", 10358 - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", 10359 - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", 10360 - "dev": true, 10361 - "license": "MIT", 10362 - "engines": { 10363 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10364 - } 10365 - }, 10366 - "node_modules/jest-resolve": { 10367 - "version": "30.2.0", 10368 - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", 10369 - "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", 10370 - "dev": true, 10371 - "license": "MIT", 10372 - "dependencies": { 10373 - "chalk": "^4.1.2", 10374 - "graceful-fs": "^4.2.11", 10375 - "jest-haste-map": "30.2.0", 10376 - "jest-pnp-resolver": "^1.2.3", 10377 - "jest-util": "30.2.0", 10378 - "jest-validate": "30.2.0", 10379 - "slash": "^3.0.0", 10380 - "unrs-resolver": "^1.7.11" 10381 - }, 10382 - "engines": { 10383 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10384 - } 10385 - }, 10386 - "node_modules/jest-resolve-dependencies": { 10387 - "version": "30.2.0", 10388 - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", 10389 - "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", 10390 - "dev": true, 10391 - "license": "MIT", 10392 - "dependencies": { 10393 - "jest-regex-util": "30.0.1", 10394 - "jest-snapshot": "30.2.0" 10395 - }, 10396 - "engines": { 10397 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10398 - } 10399 - }, 10400 - "node_modules/jest-runner": { 10401 - "version": "30.2.0", 10402 - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", 10403 - "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", 10404 - "dev": true, 10405 - "license": "MIT", 10406 - "dependencies": { 10407 - "@jest/console": "30.2.0", 10408 - "@jest/environment": "30.2.0", 10409 - "@jest/test-result": "30.2.0", 10410 - "@jest/transform": "30.2.0", 10411 - "@jest/types": "30.2.0", 10412 - "@types/node": "*", 10413 - "chalk": "^4.1.2", 10414 - "emittery": "^0.13.1", 10415 - "exit-x": "^0.2.2", 10416 - "graceful-fs": "^4.2.11", 10417 - "jest-docblock": "30.2.0", 10418 - "jest-environment-node": "30.2.0", 10419 - "jest-haste-map": "30.2.0", 10420 - "jest-leak-detector": "30.2.0", 10421 - "jest-message-util": "30.2.0", 10422 - "jest-resolve": "30.2.0", 10423 - "jest-runtime": "30.2.0", 10424 - "jest-util": "30.2.0", 10425 - "jest-watcher": "30.2.0", 10426 - "jest-worker": "30.2.0", 10427 - "p-limit": "^3.1.0", 10428 - "source-map-support": "0.5.13" 10429 - }, 10430 - "engines": { 10431 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10432 - } 10433 - }, 10434 - "node_modules/jest-runtime": { 10435 - "version": "30.2.0", 10436 - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", 10437 - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", 10438 - "dev": true, 10439 - "license": "MIT", 10440 - "dependencies": { 10441 - "@jest/environment": "30.2.0", 10442 - "@jest/fake-timers": "30.2.0", 10443 - "@jest/globals": "30.2.0", 10444 - "@jest/source-map": "30.0.1", 10445 - "@jest/test-result": "30.2.0", 10446 - "@jest/transform": "30.2.0", 10447 - "@jest/types": "30.2.0", 10448 - "@types/node": "*", 10449 - "chalk": "^4.1.2", 10450 - "cjs-module-lexer": "^2.1.0", 10451 - "collect-v8-coverage": "^1.0.2", 10452 - "glob": "^10.3.10", 10453 - "graceful-fs": "^4.2.11", 10454 - "jest-haste-map": "30.2.0", 10455 - "jest-message-util": "30.2.0", 10456 - "jest-mock": "30.2.0", 10457 - "jest-regex-util": "30.0.1", 10458 - "jest-resolve": "30.2.0", 10459 - "jest-snapshot": "30.2.0", 10460 - "jest-util": "30.2.0", 10461 - "slash": "^3.0.0", 10462 - "strip-bom": "^4.0.0" 10463 - }, 10464 - "engines": { 10465 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10466 - } 10467 - }, 10468 - "node_modules/jest-snapshot": { 10469 - "version": "30.2.0", 10470 - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", 10471 - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", 10472 - "dev": true, 10473 - "license": "MIT", 10474 - "dependencies": { 10475 - "@babel/core": "^7.27.4", 10476 - "@babel/generator": "^7.27.5", 10477 - "@babel/plugin-syntax-jsx": "^7.27.1", 10478 - "@babel/plugin-syntax-typescript": "^7.27.1", 10479 - "@babel/types": "^7.27.3", 10480 - "@jest/expect-utils": "30.2.0", 10481 - "@jest/get-type": "30.1.0", 10482 - "@jest/snapshot-utils": "30.2.0", 10483 - "@jest/transform": "30.2.0", 10484 - "@jest/types": "30.2.0", 10485 - "babel-preset-current-node-syntax": "^1.2.0", 10486 - "chalk": "^4.1.2", 10487 - "expect": "30.2.0", 10488 - "graceful-fs": "^4.2.11", 10489 - "jest-diff": "30.2.0", 10490 - "jest-matcher-utils": "30.2.0", 10491 - "jest-message-util": "30.2.0", 10492 - "jest-util": "30.2.0", 10493 - "pretty-format": "30.2.0", 10494 - "semver": "^7.7.2", 10495 - "synckit": "^0.11.8" 10496 - }, 10497 - "engines": { 10498 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10499 - } 10500 - }, 10501 - "node_modules/jest-snapshot/node_modules/ansi-styles": { 10502 - "version": "5.2.0", 10503 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10504 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10505 - "dev": true, 10506 - "license": "MIT", 10507 - "engines": { 10508 - "node": ">=10" 10509 - }, 10510 - "funding": { 10511 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10512 - } 10513 - }, 10514 - "node_modules/jest-snapshot/node_modules/pretty-format": { 10515 - "version": "30.2.0", 10516 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 10517 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 10518 - "dev": true, 10519 - "license": "MIT", 10520 - "dependencies": { 10521 - "@jest/schemas": "30.0.5", 10522 - "ansi-styles": "^5.2.0", 10523 - "react-is": "^18.3.1" 10524 - }, 10525 - "engines": { 10526 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10527 - } 10528 - }, 10529 - "node_modules/jest-snapshot/node_modules/react-is": { 10530 - "version": "18.3.1", 10531 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10532 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10533 - "dev": true, 10534 - "license": "MIT" 10535 - }, 10536 - "node_modules/jest-snapshot/node_modules/semver": { 10537 - "version": "7.7.3", 10538 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 10539 - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 10540 - "dev": true, 10541 - "license": "ISC", 10542 - "bin": { 10543 - "semver": "bin/semver.js" 10544 - }, 10545 - "engines": { 10546 - "node": ">=10" 10547 - } 10548 - }, 10549 - "node_modules/jest-util": { 10550 - "version": "30.2.0", 10551 - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", 10552 - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", 10553 - "dev": true, 10554 - "license": "MIT", 10555 - "dependencies": { 10556 - "@jest/types": "30.2.0", 10557 - "@types/node": "*", 10558 - "chalk": "^4.1.2", 10559 - "ci-info": "^4.2.0", 10560 - "graceful-fs": "^4.2.11", 10561 - "picomatch": "^4.0.2" 10562 - }, 10563 - "engines": { 10564 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10565 - } 10566 - }, 10567 - "node_modules/jest-util/node_modules/picomatch": { 10568 - "version": "4.0.3", 10569 - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 10570 - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 10571 - "dev": true, 10572 - "license": "MIT", 10573 - "engines": { 10574 - "node": ">=12" 10575 - }, 10576 - "funding": { 10577 - "url": "https://github.com/sponsors/jonschlinkert" 10578 - } 10579 - }, 10580 - "node_modules/jest-validate": { 10581 - "version": "30.2.0", 10582 - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", 10583 - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", 10584 - "dev": true, 10585 - "license": "MIT", 10586 - "dependencies": { 10587 - "@jest/get-type": "30.1.0", 10588 - "@jest/types": "30.2.0", 10589 - "camelcase": "^6.3.0", 10590 - "chalk": "^4.1.2", 10591 - "leven": "^3.1.0", 10592 - "pretty-format": "30.2.0" 10593 - }, 10594 - "engines": { 10595 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10596 - } 10597 - }, 10598 - "node_modules/jest-validate/node_modules/ansi-styles": { 10599 - "version": "5.2.0", 10600 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10601 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10602 - "dev": true, 10603 - "license": "MIT", 10604 - "engines": { 10605 - "node": ">=10" 10606 - }, 10607 - "funding": { 10608 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10609 - } 10610 - }, 10611 - "node_modules/jest-validate/node_modules/camelcase": { 10612 - "version": "6.3.0", 10613 - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 10614 - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 10615 - "dev": true, 10616 - "license": "MIT", 10617 - "engines": { 10618 - "node": ">=10" 10619 - }, 10620 - "funding": { 10621 - "url": "https://github.com/sponsors/sindresorhus" 10622 - } 10623 - }, 10624 - "node_modules/jest-validate/node_modules/pretty-format": { 10625 - "version": "30.2.0", 10626 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", 10627 - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", 10628 - "dev": true, 10629 - "license": "MIT", 10630 - "dependencies": { 10631 - "@jest/schemas": "30.0.5", 10632 - "ansi-styles": "^5.2.0", 10633 - "react-is": "^18.3.1" 10634 - }, 10635 - "engines": { 10636 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10637 - } 10638 - }, 10639 - "node_modules/jest-validate/node_modules/react-is": { 10640 - "version": "18.3.1", 10641 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10642 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10643 - "dev": true, 10644 - "license": "MIT" 10645 - }, 10646 - "node_modules/jest-watcher": { 10647 - "version": "30.2.0", 10648 - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", 10649 - "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", 10650 - "dev": true, 10651 - "license": "MIT", 10652 - "dependencies": { 10653 - "@jest/test-result": "30.2.0", 10654 - "@jest/types": "30.2.0", 10655 - "@types/node": "*", 10656 - "ansi-escapes": "^4.3.2", 10657 - "chalk": "^4.1.2", 10658 - "emittery": "^0.13.1", 10659 - "jest-util": "30.2.0", 10660 - "string-length": "^4.0.2" 10661 - }, 10662 - "engines": { 10663 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10664 - } 10665 - }, 10666 - "node_modules/jest-websocket-mock": { 10667 - "version": "2.5.0", 10668 - "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.5.0.tgz", 10669 - "integrity": "sha512-a+UJGfowNIWvtIKIQBHoEWIUqRxxQHFx4CXT+R5KxxKBtEQ5rS3pPOV/5299sHzqbmeCzxxY5qE4+yfXePePig==", 10670 - "dev": true, 10671 - "license": "MIT", 10672 - "dependencies": { 10673 - "jest-diff": "^29.2.0", 10674 - "mock-socket": "^9.3.0" 10675 - } 10676 - }, 10677 - "node_modules/jest-websocket-mock/node_modules/@jest/schemas": { 10678 - "version": "29.6.3", 10679 - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", 10680 - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", 10681 - "dev": true, 10682 - "license": "MIT", 10683 - "dependencies": { 10684 - "@sinclair/typebox": "^0.27.8" 10685 - }, 10686 - "engines": { 10687 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 10688 - } 10689 - }, 10690 - "node_modules/jest-websocket-mock/node_modules/@sinclair/typebox": { 10691 - "version": "0.27.8", 10692 - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", 10693 - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", 10694 - "dev": true, 10695 - "license": "MIT" 10696 - }, 10697 - "node_modules/jest-websocket-mock/node_modules/ansi-styles": { 10698 - "version": "5.2.0", 10699 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", 10700 - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", 10701 - "dev": true, 10702 - "license": "MIT", 10703 - "engines": { 10704 - "node": ">=10" 10705 - }, 10706 - "funding": { 10707 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 10708 - } 10709 - }, 10710 - "node_modules/jest-websocket-mock/node_modules/jest-diff": { 10711 - "version": "29.7.0", 10712 - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", 10713 - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", 10714 - "dev": true, 10715 - "license": "MIT", 10716 - "dependencies": { 10717 - "chalk": "^4.0.0", 10718 - "diff-sequences": "^29.6.3", 10719 - "jest-get-type": "^29.6.3", 10720 - "pretty-format": "^29.7.0" 10721 - }, 10722 - "engines": { 10723 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 10724 - } 10725 - }, 10726 - "node_modules/jest-websocket-mock/node_modules/pretty-format": { 10727 - "version": "29.7.0", 10728 - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", 10729 - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", 10730 - "dev": true, 10731 - "license": "MIT", 10732 - "dependencies": { 10733 - "@jest/schemas": "^29.6.3", 10734 - "ansi-styles": "^5.0.0", 10735 - "react-is": "^18.0.0" 10736 - }, 10737 - "engines": { 10738 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" 10739 - } 10740 - }, 10741 - "node_modules/jest-websocket-mock/node_modules/react-is": { 10742 - "version": "18.3.1", 10743 - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", 10744 - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", 10745 - "dev": true, 10746 - "license": "MIT" 10747 - }, 10748 - "node_modules/jest-worker": { 10749 - "version": "30.2.0", 10750 - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", 10751 - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", 10752 - "dev": true, 10753 - "license": "MIT", 10754 - "dependencies": { 10755 - "@types/node": "*", 10756 - "@ungap/structured-clone": "^1.3.0", 10757 - "jest-util": "30.2.0", 10758 - "merge-stream": "^2.0.0", 10759 - "supports-color": "^8.1.1" 10760 - }, 10761 - "engines": { 10762 - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" 10763 - } 10764 - }, 10765 - "node_modules/jest-worker/node_modules/supports-color": { 10766 - "version": "8.1.1", 10767 - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 10768 - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 10769 - "dev": true, 10770 - "license": "MIT", 10771 - "dependencies": { 10772 - "has-flag": "^4.0.0" 10773 - }, 10774 - "engines": { 10775 - "node": ">=10" 10776 - }, 10777 - "funding": { 10778 - "url": "https://github.com/chalk/supports-color?sponsor=1" 10779 - } 10780 - }, 10781 8155 "node_modules/jiti": { 10782 8156 "version": "2.6.1", 10783 8157 "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", ··· 11044 8418 "node": ">=12" 11045 8419 } 11046 8420 }, 11047 - "node_modules/leven": { 11048 - "version": "3.1.0", 11049 - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", 11050 - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", 11051 - "dev": true, 11052 - "license": "MIT", 11053 - "engines": { 11054 - "node": ">=6" 11055 - } 11056 - }, 11057 8421 "node_modules/levn": { 11058 8422 "version": "0.4.1", 11059 8423 "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", ··· 11322 8686 "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", 11323 8687 "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" 11324 8688 }, 11325 - "node_modules/lines-and-columns": { 11326 - "version": "1.2.4", 11327 - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 11328 - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 11329 - "dev": true, 11330 - "license": "MIT" 11331 - }, 11332 8689 "node_modules/linkedom": { 11333 8690 "version": "0.18.12", 11334 8691 "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", ··· 11423 8780 "dev": true, 11424 8781 "license": "MIT" 11425 8782 }, 11426 - "node_modules/lodash.memoize": { 11427 - "version": "4.1.2", 11428 - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", 11429 - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", 11430 - "dev": true, 11431 - "license": "MIT" 11432 - }, 11433 8783 "node_modules/lodash.merge": { 11434 8784 "version": "4.6.2", 11435 8785 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", ··· 11513 8863 "@jridgewell/sourcemap-codec": "^1.5.5" 11514 8864 } 11515 8865 }, 8866 + "node_modules/magicast": { 8867 + "version": "0.5.1", 8868 + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", 8869 + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", 8870 + "dev": true, 8871 + "license": "MIT", 8872 + "dependencies": { 8873 + "@babel/parser": "^7.28.5", 8874 + "@babel/types": "^7.28.5", 8875 + "source-map-js": "^1.2.1" 8876 + } 8877 + }, 11516 8878 "node_modules/magnet-uri": { 11517 8879 "version": "7.0.7", 11518 8880 "resolved": "https://registry.npmjs.org/magnet-uri/-/magnet-uri-7.0.7.tgz", ··· 11569 8931 "engines": { 11570 8932 "node": ">=10" 11571 8933 } 11572 - }, 11573 - "node_modules/make-error": { 11574 - "version": "1.3.6", 11575 - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 11576 - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 11577 - "dev": true, 11578 - "license": "ISC" 11579 8934 }, 11580 8935 "node_modules/make-fetch-happen": { 11581 8936 "version": "9.1.0", ··· 11697 9052 "dev": true, 11698 9053 "license": "ISC", 11699 9054 "optional": true 11700 - }, 11701 - "node_modules/makeerror": { 11702 - "version": "1.0.12", 11703 - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", 11704 - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", 11705 - "dev": true, 11706 - "license": "BSD-3-Clause", 11707 - "dependencies": { 11708 - "tmpl": "1.0.5" 11709 - } 11710 9055 }, 11711 9056 "node_modules/markdown-it": { 11712 9057 "version": "14.1.0", ··· 12796 10141 "node": ">= 0.6" 12797 10142 } 12798 10143 }, 12799 - "node_modules/mimic-fn": { 12800 - "version": "2.1.0", 12801 - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 12802 - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 12803 - "dev": true, 12804 - "license": "MIT", 12805 - "engines": { 12806 - "node": ">=6" 12807 - } 12808 - }, 12809 10144 "node_modules/mimic-response": { 12810 10145 "version": "3.1.0", 12811 10146 "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", ··· 12865 10200 "license": "MIT", 12866 10201 "funding": { 12867 10202 "url": "https://github.com/sponsors/ljharb" 12868 - } 12869 - }, 12870 - "node_modules/minipass": { 12871 - "version": "7.1.2", 12872 - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 12873 - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 12874 - "dev": true, 12875 - "license": "ISC", 12876 - "engines": { 12877 - "node": ">=16 || 14 >=14.17" 12878 10203 } 12879 10204 }, 12880 10205 "node_modules/minipass-collect": { ··· 13119 10444 "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 13120 10445 "license": "MIT" 13121 10446 }, 13122 - "node_modules/mock-socket": { 13123 - "version": "9.3.1", 13124 - "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", 13125 - "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", 13126 - "dev": true, 13127 - "license": "MIT", 13128 - "engines": { 13129 - "node": ">= 8" 13130 - } 13131 - }, 13132 10447 "node_modules/module-error": { 13133 10448 "version": "1.0.2", 13134 10449 "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", ··· 13174 10489 "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", 13175 10490 "license": "MIT" 13176 10491 }, 13177 - "node_modules/napi-postinstall": { 13178 - "version": "0.3.4", 13179 - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", 13180 - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", 13181 - "dev": true, 13182 - "license": "MIT", 13183 - "bin": { 13184 - "napi-postinstall": "lib/cli.js" 13185 - }, 13186 - "engines": { 13187 - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" 13188 - }, 13189 - "funding": { 13190 - "url": "https://opencollective.com/napi-postinstall" 13191 - } 13192 - }, 13193 10492 "node_modules/natural-compare": { 13194 10493 "version": "1.4.0", 13195 10494 "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", ··· 13205 10504 "engines": { 13206 10505 "node": ">= 0.6" 13207 10506 } 13208 - }, 13209 - "node_modules/neo-async": { 13210 - "version": "2.6.2", 13211 - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", 13212 - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", 13213 - "dev": true, 13214 - "license": "MIT" 13215 10507 }, 13216 10508 "node_modules/netmask": { 13217 10509 "version": "2.0.2", ··· 13442 10734 "node": ">= 8" 13443 10735 } 13444 10736 }, 13445 - "node_modules/node-int64": { 13446 - "version": "0.4.0", 13447 - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", 13448 - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", 13449 - "dev": true, 13450 - "license": "MIT" 13451 - }, 13452 10737 "node_modules/node-releases": { 13453 10738 "version": "2.0.27", 13454 10739 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", ··· 13574 10859 "node": ">=0.10.0" 13575 10860 } 13576 10861 }, 13577 - "node_modules/npm-run-path": { 13578 - "version": "4.0.1", 13579 - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 13580 - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 13581 - "dev": true, 13582 - "license": "MIT", 13583 - "dependencies": { 13584 - "path-key": "^3.0.0" 13585 - }, 13586 - "engines": { 13587 - "node": ">=8" 13588 - } 13589 - }, 13590 10862 "node_modules/npmlog": { 13591 10863 "version": "6.0.2", 13592 10864 "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", ··· 13705 10977 "wrappy": "1" 13706 10978 } 13707 10979 }, 13708 - "node_modules/onetime": { 13709 - "version": "5.1.2", 13710 - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 13711 - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 13712 - "dev": true, 13713 - "license": "MIT", 13714 - "dependencies": { 13715 - "mimic-fn": "^2.1.0" 13716 - }, 13717 - "engines": { 13718 - "node": ">=6" 13719 - }, 13720 - "funding": { 13721 - "url": "https://github.com/sponsors/sindresorhus" 13722 - } 13723 - }, 13724 10980 "node_modules/optionator": { 13725 10981 "version": "0.9.4", 13726 10982 "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", ··· 13778 11034 "url": "https://github.com/sponsors/sindresorhus" 13779 11035 } 13780 11036 }, 13781 - "node_modules/p-try": { 13782 - "version": "2.2.0", 13783 - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 13784 - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 13785 - "dev": true, 13786 - "license": "MIT", 13787 - "engines": { 13788 - "node": ">=6" 13789 - } 13790 - }, 13791 - "node_modules/package-json-from-dist": { 13792 - "version": "1.0.1", 13793 - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 13794 - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 13795 - "dev": true, 13796 - "license": "BlueOak-1.0.0" 13797 - }, 13798 11037 "node_modules/pako": { 13799 11038 "version": "1.0.11", 13800 11039 "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", ··· 13832 11071 "node": ">= 0.10" 13833 11072 } 13834 11073 }, 13835 - "node_modules/parse-gitignore": { 13836 - "version": "2.0.0", 13837 - "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", 13838 - "integrity": "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==", 13839 - "dev": true, 13840 - "license": "MIT", 13841 - "engines": { 13842 - "node": ">=14" 13843 - } 13844 - }, 13845 11074 "node_modules/parse-imports-exports": { 13846 11075 "version": "0.2.4", 13847 11076 "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", ··· 13852 11081 "parse-statements": "1.0.11" 13853 11082 } 13854 11083 }, 13855 - "node_modules/parse-json": { 13856 - "version": "5.2.0", 13857 - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", 13858 - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", 13859 - "dev": true, 13860 - "license": "MIT", 13861 - "dependencies": { 13862 - "@babel/code-frame": "^7.0.0", 13863 - "error-ex": "^1.3.1", 13864 - "json-parse-even-better-errors": "^2.3.0", 13865 - "lines-and-columns": "^1.1.6" 13866 - }, 13867 - "engines": { 13868 - "node": ">=8" 13869 - }, 13870 - "funding": { 13871 - "url": "https://github.com/sponsors/sindresorhus" 13872 - } 13873 - }, 13874 - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { 13875 - "version": "2.3.1", 13876 - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", 13877 - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", 13878 - "dev": true, 13879 - "license": "MIT" 13880 - }, 13881 11084 "node_modules/parse-statements": { 13882 11085 "version": "1.0.11", 13883 11086 "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", ··· 13977 11180 "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 13978 11181 "dev": true, 13979 11182 "license": "MIT", 11183 + "optional": true, 13980 11184 "engines": { 13981 11185 "node": ">=0.10.0" 13982 11186 } ··· 13997 11201 "dev": true, 13998 11202 "license": "MIT" 13999 11203 }, 14000 - "node_modules/path-scurry": { 14001 - "version": "1.11.1", 14002 - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 14003 - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 14004 - "dev": true, 14005 - "license": "BlueOak-1.0.0", 14006 - "dependencies": { 14007 - "lru-cache": "^10.2.0", 14008 - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 14009 - }, 14010 - "engines": { 14011 - "node": ">=16 || 14 >=14.18" 14012 - }, 14013 - "funding": { 14014 - "url": "https://github.com/sponsors/isaacs" 14015 - } 14016 - }, 14017 - "node_modules/path-scurry/node_modules/lru-cache": { 14018 - "version": "10.4.3", 14019 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 14020 - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 14021 - "dev": true, 14022 - "license": "ISC" 14023 - }, 14024 11204 "node_modules/path-to-regexp": { 14025 11205 "version": "8.3.0", 14026 11206 "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", ··· 14030 11210 "type": "opencollective", 14031 11211 "url": "https://opencollective.com/express" 14032 11212 } 11213 + }, 11214 + "node_modules/pathe": { 11215 + "version": "2.0.3", 11216 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 11217 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 11218 + "dev": true, 11219 + "license": "MIT" 14033 11220 }, 14034 11221 "node_modules/pbkdf2": { 14035 11222 "version": "3.1.5", ··· 14074 11261 "integrity": "sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug==", 14075 11262 "license": "MIT" 14076 11263 }, 14077 - "node_modules/pirates": { 14078 - "version": "4.0.7", 14079 - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", 14080 - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", 14081 - "dev": true, 14082 - "license": "MIT", 14083 - "engines": { 14084 - "node": ">= 6" 14085 - } 14086 - }, 14087 - "node_modules/pkg-dir": { 14088 - "version": "4.2.0", 14089 - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 14090 - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 14091 - "dev": true, 14092 - "license": "MIT", 14093 - "dependencies": { 14094 - "find-up": "^4.0.0" 14095 - }, 14096 - "engines": { 14097 - "node": ">=8" 14098 - } 14099 - }, 14100 - "node_modules/pkg-dir/node_modules/find-up": { 14101 - "version": "4.1.0", 14102 - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 14103 - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 14104 - "dev": true, 14105 - "license": "MIT", 14106 - "dependencies": { 14107 - "locate-path": "^5.0.0", 14108 - "path-exists": "^4.0.0" 14109 - }, 14110 - "engines": { 14111 - "node": ">=8" 14112 - } 14113 - }, 14114 - "node_modules/pkg-dir/node_modules/locate-path": { 14115 - "version": "5.0.0", 14116 - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 14117 - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 14118 - "dev": true, 14119 - "license": "MIT", 14120 - "dependencies": { 14121 - "p-locate": "^4.1.0" 14122 - }, 14123 - "engines": { 14124 - "node": ">=8" 14125 - } 14126 - }, 14127 - "node_modules/pkg-dir/node_modules/p-limit": { 14128 - "version": "2.3.0", 14129 - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 14130 - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 14131 - "dev": true, 14132 - "license": "MIT", 14133 - "dependencies": { 14134 - "p-try": "^2.0.0" 14135 - }, 14136 - "engines": { 14137 - "node": ">=6" 14138 - }, 14139 - "funding": { 14140 - "url": "https://github.com/sponsors/sindresorhus" 14141 - } 14142 - }, 14143 - "node_modules/pkg-dir/node_modules/p-locate": { 14144 - "version": "4.1.0", 14145 - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 14146 - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 14147 - "dev": true, 14148 - "license": "MIT", 14149 - "dependencies": { 14150 - "p-limit": "^2.2.0" 14151 - }, 14152 - "engines": { 14153 - "node": ">=8" 14154 - } 14155 - }, 14156 11264 "node_modules/possible-typed-array-names": { 14157 11265 "version": "1.1.0", 14158 11266 "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", ··· 14416 11524 "node": ">=6" 14417 11525 } 14418 11526 }, 14419 - "node_modules/pure-rand": { 14420 - "version": "7.0.1", 14421 - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", 14422 - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", 14423 - "dev": true, 14424 - "funding": [ 14425 - { 14426 - "type": "individual", 14427 - "url": "https://github.com/sponsors/dubzzz" 14428 - }, 14429 - { 14430 - "type": "opencollective", 14431 - "url": "https://opencollective.com/fast-check" 14432 - } 14433 - ], 14434 - "license": "MIT" 14435 - }, 14436 11527 "node_modules/qs": { 14437 11528 "version": "6.14.0", 14438 11529 "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", ··· 14685 11776 "bare": ">=1.10.0" 14686 11777 } 14687 11778 }, 14688 - "node_modules/require-directory": { 14689 - "version": "2.1.1", 14690 - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 14691 - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 14692 - "dev": true, 14693 - "license": "MIT", 14694 - "engines": { 14695 - "node": ">=0.10.0" 14696 - } 14697 - }, 14698 11779 "node_modules/require-from-string": { 14699 11780 "version": "2.0.2", 14700 11781 "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", ··· 14705 11786 "node": ">=0.10.0" 14706 11787 } 14707 11788 }, 14708 - "node_modules/resolve-cwd": { 14709 - "version": "3.0.0", 14710 - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", 14711 - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", 14712 - "dev": true, 14713 - "license": "MIT", 14714 - "dependencies": { 14715 - "resolve-from": "^5.0.0" 14716 - }, 14717 - "engines": { 14718 - "node": ">=8" 14719 - } 14720 - }, 14721 - "node_modules/resolve-cwd/node_modules/resolve-from": { 14722 - "version": "5.0.0", 14723 - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", 14724 - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", 14725 - "dev": true, 14726 - "license": "MIT", 14727 - "engines": { 14728 - "node": ">=8" 14729 - } 14730 - }, 14731 11789 "node_modules/resolve-from": { 14732 11790 "version": "4.0.0", 14733 11791 "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", ··· 15338 12396 "url": "https://github.com/sponsors/ljharb" 15339 12397 } 15340 12398 }, 15341 - "node_modules/signal-exit": { 15342 - "version": "4.1.0", 15343 - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 15344 - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 12399 + "node_modules/siginfo": { 12400 + "version": "2.0.0", 12401 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 12402 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 15345 12403 "dev": true, 15346 - "license": "ISC", 15347 - "engines": { 15348 - "node": ">=14" 15349 - }, 15350 - "funding": { 15351 - "url": "https://github.com/sponsors/isaacs" 15352 - } 12404 + "license": "ISC" 15353 12405 }, 15354 12406 "node_modules/simple-concat": { 15355 12407 "version": "1.0.1", ··· 15394 12446 "decompress-response": "^6.0.0", 15395 12447 "once": "^1.3.1", 15396 12448 "simple-concat": "^1.0.0" 15397 - } 15398 - }, 15399 - "node_modules/slash": { 15400 - "version": "3.0.0", 15401 - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", 15402 - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", 15403 - "dev": true, 15404 - "license": "MIT", 15405 - "engines": { 15406 - "node": ">=8" 15407 12449 } 15408 12450 }, 15409 12451 "node_modules/smart-buffer": { ··· 15527 12569 "solid-js": "^1.3" 15528 12570 } 15529 12571 }, 15530 - "node_modules/source-map": { 15531 - "version": "0.6.1", 15532 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 15533 - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 15534 - "dev": true, 15535 - "license": "BSD-3-Clause", 15536 - "engines": { 15537 - "node": ">=0.10.0" 15538 - } 15539 - }, 15540 12572 "node_modules/source-map-js": { 15541 12573 "version": "1.2.1", 15542 12574 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", ··· 15546 12578 "node": ">=0.10.0" 15547 12579 } 15548 12580 }, 15549 - "node_modules/source-map-support": { 15550 - "version": "0.5.13", 15551 - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", 15552 - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", 15553 - "dev": true, 15554 - "license": "MIT", 15555 - "dependencies": { 15556 - "buffer-from": "^1.0.0", 15557 - "source-map": "^0.6.0" 15558 - } 15559 - }, 15560 12581 "node_modules/speed-limiter": { 15561 12582 "version": "1.0.2", 15562 12583 "resolved": "https://registry.npmjs.org/speed-limiter/-/speed-limiter-1.0.2.tgz", ··· 15578 12599 "engines": { 15579 12600 "node": "*" 15580 12601 } 15581 - }, 15582 - "node_modules/sprintf-js": { 15583 - "version": "1.0.3", 15584 - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 15585 - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", 15586 - "dev": true, 15587 - "license": "BSD-3-Clause" 15588 12602 }, 15589 12603 "node_modules/sqlite3": { 15590 12604 "version": "5.1.7", ··· 15648 12662 "license": "ISC", 15649 12663 "optional": true 15650 12664 }, 15651 - "node_modules/stack-utils": { 15652 - "version": "2.0.6", 15653 - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", 15654 - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", 12665 + "node_modules/stackback": { 12666 + "version": "0.0.2", 12667 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 12668 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 15655 12669 "dev": true, 15656 - "license": "MIT", 15657 - "dependencies": { 15658 - "escape-string-regexp": "^2.0.0" 15659 - }, 15660 - "engines": { 15661 - "node": ">=10" 15662 - } 15663 - }, 15664 - "node_modules/stack-utils/node_modules/escape-string-regexp": { 15665 - "version": "2.0.0", 15666 - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", 15667 - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", 15668 - "dev": true, 15669 - "license": "MIT", 15670 - "engines": { 15671 - "node": ">=8" 15672 - } 12670 + "license": "MIT" 15673 12671 }, 15674 12672 "node_modules/statuses": { 15675 12673 "version": "2.0.2", ··· 15679 12677 "engines": { 15680 12678 "node": ">= 0.8" 15681 12679 } 12680 + }, 12681 + "node_modules/std-env": { 12682 + "version": "3.10.0", 12683 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", 12684 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", 12685 + "dev": true, 12686 + "license": "MIT" 15682 12687 }, 15683 12688 "node_modules/stream-browserify": { 15684 12689 "version": "3.0.0", ··· 15726 12731 "safe-buffer": "~5.2.0" 15727 12732 } 15728 12733 }, 15729 - "node_modules/string-length": { 15730 - "version": "4.0.2", 15731 - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", 15732 - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", 15733 - "dev": true, 15734 - "license": "MIT", 15735 - "dependencies": { 15736 - "char-regex": "^1.0.2", 15737 - "strip-ansi": "^6.0.0" 15738 - }, 15739 - "engines": { 15740 - "node": ">=10" 15741 - } 15742 - }, 15743 - "node_modules/string-length/node_modules/strip-ansi": { 15744 - "version": "6.0.1", 15745 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 15746 - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 15747 - "dev": true, 15748 - "license": "MIT", 15749 - "dependencies": { 15750 - "ansi-regex": "^5.0.1" 15751 - }, 15752 - "engines": { 15753 - "node": ">=8" 15754 - } 15755 - }, 15756 - "node_modules/string-width": { 15757 - "version": "5.1.2", 15758 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 15759 - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 15760 - "dev": true, 15761 - "license": "MIT", 15762 - "dependencies": { 15763 - "eastasianwidth": "^0.2.0", 15764 - "emoji-regex": "^9.2.2", 15765 - "strip-ansi": "^7.0.1" 15766 - }, 15767 - "engines": { 15768 - "node": ">=12" 15769 - }, 15770 - "funding": { 15771 - "url": "https://github.com/sponsors/sindresorhus" 15772 - } 15773 - }, 15774 - "node_modules/string-width-cjs": { 15775 - "name": "string-width", 15776 - "version": "4.2.3", 15777 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 15778 - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 15779 - "dev": true, 15780 - "license": "MIT", 15781 - "dependencies": { 15782 - "emoji-regex": "^8.0.0", 15783 - "is-fullwidth-code-point": "^3.0.0", 15784 - "strip-ansi": "^6.0.1" 15785 - }, 15786 - "engines": { 15787 - "node": ">=8" 15788 - } 15789 - }, 15790 - "node_modules/string-width-cjs/node_modules/emoji-regex": { 15791 - "version": "8.0.0", 15792 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 15793 - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 15794 - "dev": true, 15795 - "license": "MIT" 15796 - }, 15797 - "node_modules/string-width-cjs/node_modules/strip-ansi": { 15798 - "version": "6.0.1", 15799 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 15800 - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 15801 - "dev": true, 15802 - "license": "MIT", 15803 - "dependencies": { 15804 - "ansi-regex": "^5.0.1" 15805 - }, 15806 - "engines": { 15807 - "node": ">=8" 15808 - } 15809 - }, 15810 12734 "node_modules/string2compact": { 15811 12735 "version": "2.0.1", 15812 12736 "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-2.0.1.tgz", ··· 15827 12751 "license": "MIT", 15828 12752 "engines": { 15829 12753 "node": ">= 10" 15830 - } 15831 - }, 15832 - "node_modules/strip-ansi": { 15833 - "version": "7.1.2", 15834 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", 15835 - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", 15836 - "dev": true, 15837 - "license": "MIT", 15838 - "dependencies": { 15839 - "ansi-regex": "^6.0.1" 15840 - }, 15841 - "engines": { 15842 - "node": ">=12" 15843 - }, 15844 - "funding": { 15845 - "url": "https://github.com/chalk/strip-ansi?sponsor=1" 15846 - } 15847 - }, 15848 - "node_modules/strip-ansi-cjs": { 15849 - "name": "strip-ansi", 15850 - "version": "6.0.1", 15851 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 15852 - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 15853 - "dev": true, 15854 - "license": "MIT", 15855 - "dependencies": { 15856 - "ansi-regex": "^5.0.1" 15857 - }, 15858 - "engines": { 15859 - "node": ">=8" 15860 - } 15861 - }, 15862 - "node_modules/strip-ansi/node_modules/ansi-regex": { 15863 - "version": "6.2.2", 15864 - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", 15865 - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", 15866 - "dev": true, 15867 - "license": "MIT", 15868 - "engines": { 15869 - "node": ">=12" 15870 - }, 15871 - "funding": { 15872 - "url": "https://github.com/chalk/ansi-regex?sponsor=1" 15873 - } 15874 - }, 15875 - "node_modules/strip-bom": { 15876 - "version": "4.0.0", 15877 - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", 15878 - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", 15879 - "dev": true, 15880 - "license": "MIT", 15881 - "engines": { 15882 - "node": ">=8" 15883 - } 15884 - }, 15885 - "node_modules/strip-final-newline": { 15886 - "version": "2.0.0", 15887 - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 15888 - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 15889 - "dev": true, 15890 - "license": "MIT", 15891 - "engines": { 15892 - "node": ">=6" 15893 12754 } 15894 12755 }, 15895 12756 "node_modules/strip-indent": { ··· 16105 12966 "license": "ISC", 16106 12967 "optional": true 16107 12968 }, 16108 - "node_modules/test-exclude": { 16109 - "version": "6.0.0", 16110 - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", 16111 - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", 16112 - "dev": true, 16113 - "license": "ISC", 16114 - "dependencies": { 16115 - "@istanbuljs/schema": "^0.1.2", 16116 - "glob": "^7.1.4", 16117 - "minimatch": "^3.0.4" 16118 - }, 16119 - "engines": { 16120 - "node": ">=8" 16121 - } 16122 - }, 16123 - "node_modules/test-exclude/node_modules/brace-expansion": { 16124 - "version": "1.1.12", 16125 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 16126 - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 16127 - "dev": true, 16128 - "license": "MIT", 16129 - "dependencies": { 16130 - "balanced-match": "^1.0.0", 16131 - "concat-map": "0.0.1" 16132 - } 16133 - }, 16134 - "node_modules/test-exclude/node_modules/glob": { 16135 - "version": "7.2.3", 16136 - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 16137 - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 16138 - "deprecated": "Glob versions prior to v9 are no longer supported", 16139 - "dev": true, 16140 - "license": "ISC", 16141 - "dependencies": { 16142 - "fs.realpath": "^1.0.0", 16143 - "inflight": "^1.0.4", 16144 - "inherits": "2", 16145 - "minimatch": "^3.1.1", 16146 - "once": "^1.3.0", 16147 - "path-is-absolute": "^1.0.0" 16148 - }, 16149 - "engines": { 16150 - "node": "*" 16151 - }, 16152 - "funding": { 16153 - "url": "https://github.com/sponsors/isaacs" 16154 - } 16155 - }, 16156 - "node_modules/test-exclude/node_modules/minimatch": { 16157 - "version": "3.1.2", 16158 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 16159 - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 16160 - "dev": true, 16161 - "license": "ISC", 16162 - "dependencies": { 16163 - "brace-expansion": "^1.1.7" 16164 - }, 16165 - "engines": { 16166 - "node": "*" 16167 - } 16168 - }, 16169 12969 "node_modules/text-decoder": { 16170 12970 "version": "1.2.3", 16171 12971 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", ··· 16244 13044 "dev": true, 16245 13045 "license": "Apache 2" 16246 13046 }, 13047 + "node_modules/tinybench": { 13048 + "version": "2.9.0", 13049 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 13050 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 13051 + "dev": true, 13052 + "license": "MIT" 13053 + }, 13054 + "node_modules/tinyexec": { 13055 + "version": "0.3.2", 13056 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 13057 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 13058 + "dev": true, 13059 + "license": "MIT" 13060 + }, 16247 13061 "node_modules/tinyglobby": { 16248 13062 "version": "0.2.15", 16249 13063 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", ··· 16290 13104 "url": "https://github.com/sponsors/jonschlinkert" 16291 13105 } 16292 13106 }, 13107 + "node_modules/tinyrainbow": { 13108 + "version": "3.0.3", 13109 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", 13110 + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", 13111 + "dev": true, 13112 + "license": "MIT", 13113 + "engines": { 13114 + "node": ">=14.0.0" 13115 + } 13116 + }, 16293 13117 "node_modules/tldts": { 16294 13118 "version": "6.1.86", 16295 13119 "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", ··· 16309 13133 "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", 16310 13134 "dev": true, 16311 13135 "license": "MIT" 16312 - }, 16313 - "node_modules/tmpl": { 16314 - "version": "1.0.5", 16315 - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", 16316 - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", 16317 - "dev": true, 16318 - "license": "BSD-3-Clause" 16319 13136 }, 16320 13137 "node_modules/to-buffer": { 16321 13138 "version": "1.2.2", ··· 16466 13283 "typescript": ">=4.8.4" 16467 13284 } 16468 13285 }, 16469 - "node_modules/ts-jest": { 16470 - "version": "29.4.5", 16471 - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", 16472 - "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", 16473 - "dev": true, 16474 - "license": "MIT", 16475 - "dependencies": { 16476 - "bs-logger": "^0.2.6", 16477 - "fast-json-stable-stringify": "^2.1.0", 16478 - "handlebars": "^4.7.8", 16479 - "json5": "^2.2.3", 16480 - "lodash.memoize": "^4.1.2", 16481 - "make-error": "^1.3.6", 16482 - "semver": "^7.7.3", 16483 - "type-fest": "^4.41.0", 16484 - "yargs-parser": "^21.1.1" 16485 - }, 16486 - "bin": { 16487 - "ts-jest": "cli.js" 16488 - }, 16489 - "engines": { 16490 - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" 16491 - }, 16492 - "peerDependencies": { 16493 - "@babel/core": ">=7.0.0-beta.0 <8", 16494 - "@jest/transform": "^29.0.0 || ^30.0.0", 16495 - "@jest/types": "^29.0.0 || ^30.0.0", 16496 - "babel-jest": "^29.0.0 || ^30.0.0", 16497 - "jest": "^29.0.0 || ^30.0.0", 16498 - "jest-util": "^29.0.0 || ^30.0.0", 16499 - "typescript": ">=4.3 <6" 16500 - }, 16501 - "peerDependenciesMeta": { 16502 - "@babel/core": { 16503 - "optional": true 16504 - }, 16505 - "@jest/transform": { 16506 - "optional": true 16507 - }, 16508 - "@jest/types": { 16509 - "optional": true 16510 - }, 16511 - "babel-jest": { 16512 - "optional": true 16513 - }, 16514 - "esbuild": { 16515 - "optional": true 16516 - }, 16517 - "jest-util": { 16518 - "optional": true 16519 - } 16520 - } 16521 - }, 16522 - "node_modules/ts-jest/node_modules/semver": { 16523 - "version": "7.7.3", 16524 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 16525 - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 16526 - "dev": true, 16527 - "license": "ISC", 16528 - "bin": { 16529 - "semver": "bin/semver.js" 16530 - }, 16531 - "engines": { 16532 - "node": ">=10" 16533 - } 16534 - }, 16535 - "node_modules/ts-jest/node_modules/type-fest": { 16536 - "version": "4.41.0", 16537 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", 16538 - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", 16539 - "dev": true, 16540 - "license": "(MIT OR CC0-1.0)", 16541 - "engines": { 16542 - "node": ">=16" 16543 - }, 16544 - "funding": { 16545 - "url": "https://github.com/sponsors/sindresorhus" 16546 - } 16547 - }, 16548 13286 "node_modules/ts-pattern": { 16549 13287 "version": "5.9.0", 16550 13288 "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.9.0.tgz", ··· 16620 13358 "node": ">= 0.8.0" 16621 13359 } 16622 13360 }, 16623 - "node_modules/type-detect": { 16624 - "version": "4.0.8", 16625 - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 16626 - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 16627 - "dev": true, 16628 - "license": "MIT", 16629 - "engines": { 16630 - "node": ">=4" 16631 - } 16632 - }, 16633 - "node_modules/type-fest": { 16634 - "version": "0.21.3", 16635 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", 16636 - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", 16637 - "dev": true, 16638 - "license": "(MIT OR CC0-1.0)", 16639 - "engines": { 16640 - "node": ">=10" 16641 - }, 16642 - "funding": { 16643 - "url": "https://github.com/sponsors/sindresorhus" 16644 - } 16645 - }, 16646 13361 "node_modules/type-is": { 16647 13362 "version": "2.0.1", 16648 13363 "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", ··· 16826 13541 "dev": true, 16827 13542 "license": "MIT" 16828 13543 }, 16829 - "node_modules/uglify-js": { 16830 - "version": "3.19.3", 16831 - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", 16832 - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", 16833 - "dev": true, 16834 - "license": "BSD-2-Clause", 16835 - "optional": true, 16836 - "bin": { 16837 - "uglifyjs": "bin/uglifyjs" 16838 - }, 16839 - "engines": { 16840 - "node": ">=0.8.0" 16841 - } 16842 - }, 16843 13544 "node_modules/uhyphen": { 16844 13545 "version": "0.2.0", 16845 13546 "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", ··· 16978 13679 "node": ">= 0.8" 16979 13680 } 16980 13681 }, 16981 - "node_modules/unrs-resolver": { 16982 - "version": "1.11.1", 16983 - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", 16984 - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", 16985 - "dev": true, 16986 - "hasInstallScript": true, 16987 - "license": "MIT", 16988 - "dependencies": { 16989 - "napi-postinstall": "^0.3.0" 16990 - }, 16991 - "funding": { 16992 - "url": "https://opencollective.com/unrs-resolver" 16993 - }, 16994 - "optionalDependencies": { 16995 - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", 16996 - "@unrs/resolver-binding-android-arm64": "1.11.1", 16997 - "@unrs/resolver-binding-darwin-arm64": "1.11.1", 16998 - "@unrs/resolver-binding-darwin-x64": "1.11.1", 16999 - "@unrs/resolver-binding-freebsd-x64": "1.11.1", 17000 - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", 17001 - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", 17002 - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", 17003 - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", 17004 - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", 17005 - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", 17006 - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", 17007 - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", 17008 - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", 17009 - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", 17010 - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", 17011 - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", 17012 - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", 17013 - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" 17014 - } 17015 - }, 17016 13682 "node_modules/update-browserslist-db": { 17017 13683 "version": "1.1.4", 17018 13684 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", ··· 17185 13851 }, 17186 13852 "engines": { 17187 13853 "node": ">=8.12" 17188 - } 17189 - }, 17190 - "node_modules/v8-to-istanbul": { 17191 - "version": "9.3.0", 17192 - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", 17193 - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", 17194 - "dev": true, 17195 - "license": "ISC", 17196 - "dependencies": { 17197 - "@jridgewell/trace-mapping": "^0.3.12", 17198 - "@types/istanbul-lib-coverage": "^2.0.1", 17199 - "convert-source-map": "^2.0.0" 17200 - }, 17201 - "engines": { 17202 - "node": ">=10.12.0" 17203 13854 } 17204 13855 }, 17205 13856 "node_modules/vary": { ··· 17496 14147 } 17497 14148 } 17498 14149 }, 14150 + "node_modules/vitest": { 14151 + "version": "4.0.10", 14152 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.10.tgz", 14153 + "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==", 14154 + "dev": true, 14155 + "license": "MIT", 14156 + "peer": true, 14157 + "dependencies": { 14158 + "@vitest/expect": "4.0.10", 14159 + "@vitest/mocker": "4.0.10", 14160 + "@vitest/pretty-format": "4.0.10", 14161 + "@vitest/runner": "4.0.10", 14162 + "@vitest/snapshot": "4.0.10", 14163 + "@vitest/spy": "4.0.10", 14164 + "@vitest/utils": "4.0.10", 14165 + "debug": "^4.4.3", 14166 + "es-module-lexer": "^1.7.0", 14167 + "expect-type": "^1.2.2", 14168 + "magic-string": "^0.30.21", 14169 + "pathe": "^2.0.3", 14170 + "picomatch": "^4.0.3", 14171 + "std-env": "^3.10.0", 14172 + "tinybench": "^2.9.0", 14173 + "tinyexec": "^0.3.2", 14174 + "tinyglobby": "^0.2.15", 14175 + "tinyrainbow": "^3.0.3", 14176 + "vite": "^6.0.0 || ^7.0.0", 14177 + "why-is-node-running": "^2.3.0" 14178 + }, 14179 + "bin": { 14180 + "vitest": "vitest.mjs" 14181 + }, 14182 + "engines": { 14183 + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" 14184 + }, 14185 + "funding": { 14186 + "url": "https://opencollective.com/vitest" 14187 + }, 14188 + "peerDependencies": { 14189 + "@edge-runtime/vm": "*", 14190 + "@types/debug": "^4.1.12", 14191 + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", 14192 + "@vitest/browser-playwright": "4.0.10", 14193 + "@vitest/browser-preview": "4.0.10", 14194 + "@vitest/browser-webdriverio": "4.0.10", 14195 + "@vitest/ui": "4.0.10", 14196 + "happy-dom": "*", 14197 + "jsdom": "*" 14198 + }, 14199 + "peerDependenciesMeta": { 14200 + "@edge-runtime/vm": { 14201 + "optional": true 14202 + }, 14203 + "@types/debug": { 14204 + "optional": true 14205 + }, 14206 + "@types/node": { 14207 + "optional": true 14208 + }, 14209 + "@vitest/browser-playwright": { 14210 + "optional": true 14211 + }, 14212 + "@vitest/browser-preview": { 14213 + "optional": true 14214 + }, 14215 + "@vitest/browser-webdriverio": { 14216 + "optional": true 14217 + }, 14218 + "@vitest/ui": { 14219 + "optional": true 14220 + }, 14221 + "happy-dom": { 14222 + "optional": true 14223 + }, 14224 + "jsdom": { 14225 + "optional": true 14226 + } 14227 + } 14228 + }, 14229 + "node_modules/vitest/node_modules/picomatch": { 14230 + "version": "4.0.3", 14231 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 14232 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 14233 + "dev": true, 14234 + "license": "MIT", 14235 + "engines": { 14236 + "node": ">=12" 14237 + }, 14238 + "funding": { 14239 + "url": "https://github.com/sponsors/jonschlinkert" 14240 + } 14241 + }, 17499 14242 "node_modules/vm-browserify": { 17500 14243 "version": "1.1.2", 17501 14244 "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", ··· 17523 14266 "node": ">=18" 17524 14267 } 17525 14268 }, 17526 - "node_modules/walker": { 17527 - "version": "1.0.8", 17528 - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", 17529 - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", 17530 - "dev": true, 17531 - "license": "Apache-2.0", 17532 - "dependencies": { 17533 - "makeerror": "1.0.12" 17534 - } 17535 - }, 17536 14269 "node_modules/web-streams-polyfill": { 17537 14270 "version": "3.3.3", 17538 14271 "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", ··· 17714 14447 "url": "https://github.com/sponsors/ljharb" 17715 14448 } 17716 14449 }, 14450 + "node_modules/why-is-node-running": { 14451 + "version": "2.3.0", 14452 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 14453 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 14454 + "dev": true, 14455 + "license": "MIT", 14456 + "dependencies": { 14457 + "siginfo": "^2.0.0", 14458 + "stackback": "0.0.2" 14459 + }, 14460 + "bin": { 14461 + "why-is-node-running": "cli.js" 14462 + }, 14463 + "engines": { 14464 + "node": ">=8" 14465 + } 14466 + }, 17717 14467 "node_modules/wide-align": { 17718 14468 "version": "1.1.5", 17719 14469 "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", ··· 17871 14621 "node": ">=0.10.0" 17872 14622 } 17873 14623 }, 17874 - "node_modules/wordwrap": { 17875 - "version": "1.0.0", 17876 - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 17877 - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", 17878 - "dev": true, 17879 - "license": "MIT" 17880 - }, 17881 - "node_modules/wrap-ansi": { 17882 - "version": "8.1.0", 17883 - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 17884 - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 17885 - "dev": true, 17886 - "license": "MIT", 17887 - "dependencies": { 17888 - "ansi-styles": "^6.1.0", 17889 - "string-width": "^5.0.1", 17890 - "strip-ansi": "^7.0.1" 17891 - }, 17892 - "engines": { 17893 - "node": ">=12" 17894 - }, 17895 - "funding": { 17896 - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 17897 - } 17898 - }, 17899 - "node_modules/wrap-ansi-cjs": { 17900 - "name": "wrap-ansi", 17901 - "version": "7.0.0", 17902 - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 17903 - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 17904 - "dev": true, 17905 - "license": "MIT", 17906 - "dependencies": { 17907 - "ansi-styles": "^4.0.0", 17908 - "string-width": "^4.1.0", 17909 - "strip-ansi": "^6.0.0" 17910 - }, 17911 - "engines": { 17912 - "node": ">=10" 17913 - }, 17914 - "funding": { 17915 - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 17916 - } 17917 - }, 17918 - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 17919 - "version": "8.0.0", 17920 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 17921 - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 17922 - "dev": true, 17923 - "license": "MIT" 17924 - }, 17925 - "node_modules/wrap-ansi-cjs/node_modules/string-width": { 17926 - "version": "4.2.3", 17927 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 17928 - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 17929 - "dev": true, 17930 - "license": "MIT", 17931 - "dependencies": { 17932 - "emoji-regex": "^8.0.0", 17933 - "is-fullwidth-code-point": "^3.0.0", 17934 - "strip-ansi": "^6.0.1" 17935 - }, 17936 - "engines": { 17937 - "node": ">=8" 17938 - } 17939 - }, 17940 - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 17941 - "version": "6.0.1", 17942 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 17943 - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 17944 - "dev": true, 17945 - "license": "MIT", 17946 - "dependencies": { 17947 - "ansi-regex": "^5.0.1" 17948 - }, 17949 - "engines": { 17950 - "node": ">=8" 17951 - } 17952 - }, 17953 - "node_modules/wrap-ansi/node_modules/ansi-styles": { 17954 - "version": "6.2.3", 17955 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", 17956 - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", 17957 - "dev": true, 17958 - "license": "MIT", 17959 - "engines": { 17960 - "node": ">=12" 17961 - }, 17962 - "funding": { 17963 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 17964 - } 17965 - }, 17966 14624 "node_modules/wrappy": { 17967 14625 "version": "1.0.2", 17968 14626 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 17969 14627 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 17970 14628 "license": "ISC" 17971 - }, 17972 - "node_modules/write-file-atomic": { 17973 - "version": "5.0.1", 17974 - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", 17975 - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", 17976 - "dev": true, 17977 - "license": "ISC", 17978 - "dependencies": { 17979 - "imurmurhash": "^0.1.4", 17980 - "signal-exit": "^4.0.1" 17981 - }, 17982 - "engines": { 17983 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" 17984 - } 17985 14629 }, 17986 14630 "node_modules/ws": { 17987 14631 "version": "8.18.3", ··· 18054 14698 "node": ">=0.4" 18055 14699 } 18056 14700 }, 18057 - "node_modules/y18n": { 18058 - "version": "5.0.8", 18059 - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 18060 - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 18061 - "dev": true, 18062 - "license": "ISC", 18063 - "engines": { 18064 - "node": ">=10" 18065 - } 18066 - }, 18067 14701 "node_modules/yallist": { 18068 14702 "version": "3.1.1", 18069 14703 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", ··· 18082 14716 }, 18083 14717 "engines": { 18084 14718 "node": ">= 14.6" 18085 - } 18086 - }, 18087 - "node_modules/yargs": { 18088 - "version": "17.7.2", 18089 - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 18090 - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 18091 - "dev": true, 18092 - "license": "MIT", 18093 - "dependencies": { 18094 - "cliui": "^8.0.1", 18095 - "escalade": "^3.1.1", 18096 - "get-caller-file": "^2.0.5", 18097 - "require-directory": "^2.1.1", 18098 - "string-width": "^4.2.3", 18099 - "y18n": "^5.0.5", 18100 - "yargs-parser": "^21.1.1" 18101 - }, 18102 - "engines": { 18103 - "node": ">=12" 18104 - } 18105 - }, 18106 - "node_modules/yargs-parser": { 18107 - "version": "21.1.1", 18108 - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 18109 - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 18110 - "dev": true, 18111 - "license": "ISC", 18112 - "engines": { 18113 - "node": ">=12" 18114 - } 18115 - }, 18116 - "node_modules/yargs/node_modules/emoji-regex": { 18117 - "version": "8.0.0", 18118 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 18119 - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 18120 - "dev": true, 18121 - "license": "MIT" 18122 - }, 18123 - "node_modules/yargs/node_modules/string-width": { 18124 - "version": "4.2.3", 18125 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 18126 - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 18127 - "dev": true, 18128 - "license": "MIT", 18129 - "dependencies": { 18130 - "emoji-regex": "^8.0.0", 18131 - "is-fullwidth-code-point": "^3.0.0", 18132 - "strip-ansi": "^6.0.1" 18133 - }, 18134 - "engines": { 18135 - "node": ">=8" 18136 - } 18137 - }, 18138 - "node_modules/yargs/node_modules/strip-ansi": { 18139 - "version": "6.0.1", 18140 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 18141 - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 18142 - "dev": true, 18143 - "license": "MIT", 18144 - "dependencies": { 18145 - "ansi-regex": "^5.0.1" 18146 - }, 18147 - "engines": { 18148 - "node": ">=8" 18149 14719 } 18150 14720 }, 18151 14721 "node_modules/yocto-queue": {
+7 -15
package.json
··· 23 23 "dependencies": { 24 24 "@mozilla/readability": "^0.6.0", 25 25 "@tailwindcss/vite": "^4.1.16", 26 - "better-sqlite3": "^12.4.1", 27 26 "clsx": "^2.1.1", 28 27 "dexie": "^4.2.1", 29 28 "express": "^5.1.0", ··· 50 49 "@eslint/json": "~0.12.0", 51 50 "@eslint/markdown": "~6.5.0", 52 51 "@faker-js/faker": "^9.8.0", 53 - "@jest/globals": "^30.0.0", 54 52 "@testing-library/jest-dom": "^6.6.3", 55 53 "@trivago/prettier-plugin-sort-imports": "^6.0.0", 56 - "@types/better-sqlite3": "^7.6.13", 57 54 "@types/confusing-browser-globals": "^1.0.3", 58 55 "@types/express": "^5.0.3", 59 - "@types/jest": "^30.0.0", 60 56 "@types/level": "^6.0.3", 61 57 "@types/node": "^24.0.1", 62 58 "@types/ws": "^8.18.1", 59 + "@vitejs/plugin-basic-ssl": "^2.1.0", 60 + "@vitest/coverage-v8": "^4.0.10", 63 61 "confusing-browser-globals": "^1.0.11", 64 62 "eslint": "^9.28.0", 65 63 "eslint-config-prettier": "^10.1.5", 66 64 "eslint-plugin-prettier": "^5.5.0", 67 65 "eslint-plugin-solid": "^0.14.5", 68 66 "eslint-plugin-tsdoc": "^0.4.0", 69 - "glob-to-regexp": "^0.4.1", 70 67 "globals": "^16.2.0", 71 68 "identity-obj-proxy": "^3.0.0", 72 69 "indexeddbshim": "^16.0.0", 73 - "jest": "^30.0.2", 74 - "jest-environment-jsdom": "^30.0.0", 75 - "jest-fixed-jsdom": "^0.0.9", 76 - "jest-websocket-mock": "^2.5.0", 77 70 "jsdom": "^26.1.0", 78 71 "memfs": "^4.50.0", 79 - "parse-gitignore": "^2.0.0", 80 72 "prettier": "^3.5.3", 81 73 "prettier-plugin-organize-imports": "^4.3.0", 82 74 "solid-devtools": "^0.34.3", 83 - "ts-jest": "^29.4.0", 84 75 "tw-animate-css": "^1.4.0", 85 76 "typedoc": "^0.28.14", 86 77 "typedoc-plugin-markdown": "^4.9.0", ··· 95 86 "vite-plugin-checker": "^0.11.0", 96 87 "vite-plugin-node-polyfills": "^0.24.0", 97 88 "vite-plugin-solid": "^2.11.8", 89 + "vitest": "^4.0.10", 98 90 "wireit": "^0.14.12", 99 91 "zod-schema-faker": "^2.0.0-beta.5" 100 92 }, ··· 131 123 "output": [] 132 124 }, 133 125 "test": { 134 - "command": "jest --cache --cacheDirectory .jestcache", 126 + "command": "vitest run --coverage", 135 127 "files": [ 136 128 "src/**/*.{ts,tsx}", 137 - "jest.config.js", 138 - "jest.setup.js" 129 + "vitest.config.ts", 130 + "vitest.setup.ts" 139 131 ], 140 132 "output": [ 141 133 "coverage/**" ··· 157 149 }, 158 150 "run:tests": { 159 151 "service": true, 160 - "command": "jest --watch" 152 + "command": "vitest" 161 153 }, 162 154 "run:backend": { 163 155 "service": true,
+1 -1
src/lib/async/aborts.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {combineSignals, timeoutSignal} from '#lib/async/aborts' 4 4
+1 -1
src/lib/async/blocking-atom.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {BlockingAtom} from '#lib/async/blocking-atom' 4 4
+1 -1
src/lib/async/blocking-queue.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {BlockingQueue} from '#lib/async/blocking-queue' 4 4
+1 -1
src/lib/async/semaphore.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {Semaphore} from '#lib/async/semaphore' 4 4
+1 -1
src/lib/async/sleep.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {backoff, sleep} from '#lib/async/sleep' 4 4
+15 -15
src/lib/breaker.spec.ts
··· 1 - import {describe, expect, it, jest} from '@jest/globals' 1 + import {describe, expect, it, vi} from 'vitest' 2 2 3 3 import {Breaker} from '#lib/breaker' 4 4 5 5 describe('Breaker', () => { 6 6 it('allows wrapped functions to run before tripped', () => { 7 7 const breaker = new Breaker() 8 - const fn = jest.fn() 8 + const fn = vi.fn() 9 9 10 10 const wrapped = breaker.untilTripped(fn) 11 11 ··· 17 17 18 18 it('prevents wrapped functions from running after tripped', () => { 19 19 const breaker = new Breaker() 20 - const fn1 = jest.fn() 21 - const fn2 = jest.fn() 20 + const fn1 = vi.fn() 21 + const fn2 = vi.fn() 22 22 23 23 const wrapped1 = breaker.tripThen(fn1) 24 24 const wrapped2 = breaker.untilTripped(fn2) ··· 33 33 34 34 it('tripThen only allows function to run once', () => { 35 35 const breaker = new Breaker() 36 - const fn = jest.fn() 36 + const fn = vi.fn() 37 37 38 38 const wrapped = breaker.tripThen(fn) 39 39 ··· 47 47 48 48 it('untilTripped allows function to run multiple times until tripped', () => { 49 49 const breaker = new Breaker() 50 - const fn = jest.fn() 51 - const trigger = jest.fn() 50 + const fn = vi.fn() 51 + const trigger = vi.fn() 52 52 53 53 const wrapped = breaker.untilTripped(fn) 54 54 const trip = breaker.tripThen(trigger) ··· 65 65 }) 66 66 67 67 it('calls onTripped callback when tripped', () => { 68 - const onTripped = jest.fn() 68 + const onTripped = vi.fn() 69 69 const breaker = new Breaker(onTripped) 70 - const fn = jest.fn() 70 + const fn = vi.fn() 71 71 72 72 const wrapped = breaker.tripThen(fn) 73 73 ··· 79 79 80 80 it('calls onTripped before wrapped function', () => { 81 81 const callOrder: string[] = [] 82 - const onTripped = jest.fn(() => callOrder.push('onTripped')) 82 + const onTripped = vi.fn(() => callOrder.push('onTripped')) 83 83 const breaker = new Breaker(onTripped) 84 - const fn = jest.fn(() => callOrder.push('fn')) 84 + const fn = vi.fn(() => callOrder.push('fn')) 85 85 86 86 const wrapped = breaker.tripThen(fn) 87 87 ··· 103 103 104 104 it('multiple tripThen wrappers share state', () => { 105 105 const breaker = new Breaker() 106 - const fn1 = jest.fn() 107 - const fn2 = jest.fn() 106 + const fn1 = vi.fn() 107 + const fn2 = vi.fn() 108 108 109 109 const wrapped1 = breaker.tripThen(fn1) 110 110 const wrapped2 = breaker.tripThen(fn2) ··· 118 118 119 119 it('handles functions with no arguments', () => { 120 120 const breaker = new Breaker() 121 - const fn = jest.fn() 121 + const fn = vi.fn() 122 122 123 123 const wrapped = breaker.untilTripped(fn) 124 124 ··· 130 130 131 131 it('preserves function arguments types', () => { 132 132 const breaker = new Breaker() 133 - const fn = jest.fn() 133 + const fn = vi.fn() 134 134 135 135 const wrapped = breaker.untilTripped(fn) 136 136
+26 -19
src/lib/client/webrtc.ts
··· 87 87 this.#chan = this.#peer.createDataChannel('data', {ordered: true, maxRetransmits: 10}) 88 88 this.#setupDataChannel() 89 89 this.#advertiseDataChannel().catch((exc: unknown) => { 90 - console.error('unexpected error in data channel offer', exc) 91 90 this.dispatchCustomEvent('error', normalizeError(exc)) 92 91 }) 93 92 } else { ··· 121 120 #onPeerState = () => { 122 121 const state = this.#peer.connectionState 123 122 if (state === 'failed') { 124 - this.#restartIce().catch(() => { 125 - this.dispatchCustomEvent('error', new Error('ice restart failed')) 126 - this.dispatchCustomEvent('close') 127 - }) 123 + // wait 1 second for it to work again, then try restarting 124 + timeoutAbort( 125 + () => { 126 + if (this.#peer.connectionState !== 'failed') { 127 + console.log('restarting failed peer connection...') 128 + this.#restartIce().catch(() => { 129 + this.dispatchCustomEvent('error', new Error('ice restart failed')) 130 + this.dispatchCustomEvent('close') 131 + }) 132 + } 133 + }, 134 + 1000, 135 + this.#abort.signal, 136 + ) 128 137 } else if (state === 'disconnected') { 138 + // wait 1 second for it to come back again, then try restarting 129 139 timeoutAbort( 130 140 () => { 131 141 if (this.#peer.connectionState === 'disconnected') { 132 - this.dispatchCustomEvent('close') 142 + console.log('restarting disconnected peer connection...') 143 + this.#restartIce().catch(() => { 144 + this.dispatchCustomEvent('error', new Error('ice restart failed')) 145 + this.dispatchCustomEvent('close') 146 + }) 133 147 } 134 148 }, 135 - 5_000, 149 + 1000, 136 150 this.#abort.signal, 137 151 ) 138 152 } else if (state === 'closed') { 153 + console.log('closing peer connection...') 139 154 this.dispatchCustomEvent('close') 140 155 } 141 156 } 142 157 143 158 async #restartIce() { 144 - console.log('restarting ice negotiation for', this.#peer) 145 - 146 159 // nobody yet, we're starting over 147 160 this.#candidates = [] 148 161 ··· 159 172 160 173 const opts = {signal: this.#abort.signal} 161 174 162 - this.#chan.addEventListener( 163 - 'open', 164 - () => { 165 - this.dispatchCustomEvent('connect') 166 - }, 167 - opts, 168 - ) 169 - 175 + this.#chan.addEventListener('open', () => this.dispatchCustomEvent('connect'), opts) 170 176 this.#chan.addEventListener('close', () => this.dispatchCustomEvent('close'), opts) 171 177 this.#chan.addEventListener('error', (e) => this.dispatchCustomEvent('error', normalizeError(e)), opts) 172 178 this.#chan.addEventListener( ··· 185 191 async #advertiseDataChannel() { 186 192 try { 187 193 this.#pendingOffer = await this.#peer.createOffer() 194 + 188 195 await this.#peer.setLocalDescription(this.#pendingOffer) 189 196 if (this.#peer.localDescription) { 190 197 this.dispatchCustomEvent('signal', {type: 'offer', sdp: this.#peer.localDescription.toJSON()}) ··· 218 225 219 226 this.#ignoreOffer = !this.#polite && offerCollision 220 227 if (this.#ignoreOffer) { 221 - console.log('ignoring offer due to glare (impolite peer)') 228 + console.warn('ignoring offer due to glare (impolite peer)') 222 229 return 223 230 } 224 231 225 232 if (offerCollision) { 226 - console.log('offer collision detected, rolling back (polite peer)') 233 + console.warn('offer collision detected, rolling back (polite peer)') 227 234 await this.#peer.setLocalDescription({type: 'rollback'}) 228 235 } 229 236
+1 -1
src/lib/crypto/cipher.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {Cipher} from '#lib/crypto/cipher' 4 4
+1 -1
src/lib/crypto/jwks.spec.ts
··· 1 1 import * as jose from 'jose' 2 - import {describe, expect, it} from '@jest/globals' 2 + import {describe, expect, it} from 'vitest' 3 3 4 4 import {generateSignableJwt, generateSigningJwkPair, jwkExport, jwkImport, jwkSchema} from '#lib/crypto/jwks' 5 5
+1 -1
src/lib/crypto/jwts.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 import {z} from 'zod/v4' 3 3 4 4 import {generateSignableJwt, generateSigningJwkPair} from '#lib/crypto/jwks'
+1 -1
src/lib/errors.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {ProtocolError, isProtocolError, normalizeProtocolError} from '#lib/errors' 4 4
+1 -1
src/lib/schema/brand.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {Brand} from '#lib/schema/brand' 4 4
+1 -1
src/lib/schema/json.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 import {z} from 'zod/v4' 3 3 4 4 import {jsonCodec} from './json'
+33 -26
src/lib/socket.spec.ts
··· 1 - import {afterEach, beforeEach, describe, expect, it} from '@jest/globals' 2 1 import {WebSocket} from 'isomorphic-ws' 3 - import WS from 'jest-websocket-mock' 2 + import {afterEach, beforeEach, describe, expect, it} from 'vitest' 4 3 import {z} from 'zod/v4' 5 4 5 + import {MockServer, MockWebSocketServer, createMockServer} from '#spec/helpers-socket' 6 + 6 7 import {streamSocket, streamSocketSchema, takeSocket, takeSocketSchema} from '#lib/socket' 7 8 8 9 import {jsonCodec} from './schema' 9 10 import {assertParsed} from './utils' 10 11 11 - let server: WS 12 + let server: MockWebSocketServer 12 13 const TEST_URL = 'ws://localhost:1234' 13 14 14 15 beforeEach(() => { 15 - server = new WS(TEST_URL) 16 + server = createMockServer(TEST_URL) 16 17 }) 17 18 18 19 afterEach(() => { 19 - WS.clean() 20 + MockServer.clean(server) 20 21 }) 21 22 22 23 async function createConnectedWebSocket(): Promise<WebSocket> { ··· 30 31 const ws = await createConnectedWebSocket() 31 32 const promise = takeSocket(ws) 32 33 33 - server.send('test message') 34 + MockServer.send(server, 'test message') 34 35 35 36 const result = await promise 36 37 ··· 55 56 56 57 const promise = takeSocket(ws) 57 58 58 - server.close() 59 + MockServer.close(server) 59 60 60 61 await expect(promise).rejects.toThrow('socket closed') 61 62 }) ··· 65 66 66 67 const promise = takeSocket(ws) 67 68 68 - server.send('test') 69 + MockServer.send(server, 'test') 69 70 70 71 await promise 71 72 ··· 80 81 const schema = z.object({key: z.string()}) 81 82 82 83 const promise = takeSocketSchema(ws, jsonCodec(schema)) 83 - server.send('{"key":"value"}') 84 + MockServer.send(server, '{"key":"value"}') 84 85 85 86 const result = await promise 86 87 expect(result).toEqual({key: 'value'}) ··· 91 92 const schema = z.object({key: z.string()}) 92 93 93 94 const promise = takeSocketSchema(ws, jsonCodec(schema)) 94 - server.send('not json') 95 + MockServer.send(server, 'not json') 95 96 96 97 await expect(promise).rejects.toThrow() 97 98 }) ··· 101 102 const schema = z.object({key: z.string(), num: z.number()}) 102 103 103 104 const promise = takeSocketSchema(ws, jsonCodec(schema)) 104 - server.send('{"key":"value"}') 105 + MockServer.send(server, '{"key":"value"}') 105 106 106 107 await expect(promise).rejects.toThrow() 107 108 }) ··· 121 122 } 122 123 })() 123 124 124 - server.send('msg1') 125 - server.send('msg2') 126 - server.send('msg3') 125 + MockServer.send(server, 'msg1') 126 + MockServer.send(server, 'msg2') 127 + MockServer.send(server, 'msg3') 127 128 128 129 await streamPromise 129 130 ··· 140 141 } 141 142 })() 142 143 143 - server.send('msg1') 144 - server.close() 144 + MockServer.send(server, 'msg1') 145 + MockServer.close(server) 145 146 146 147 await streamPromise 147 148 148 149 expect(messages).toEqual(['msg1']) 149 150 }) 150 151 151 - it('throws on socket error', async () => { 152 + it('handles abnormal server close gracefully', async () => { 152 153 const ws = await createConnectedWebSocket() 154 + const messages: unknown[] = [] 155 + 153 156 const streamPromise = (async () => { 154 - for await (const _ of streamSocket(ws)) { 155 - // Should throw before getting here 157 + for await (const msg of streamSocket(ws)) { 158 + messages.push(msg) 156 159 } 157 160 })() 158 161 159 - server.error() 162 + MockServer.send(server, 'msg1') 163 + MockServer.error(server) // closes with abnormal code 164 + 165 + await streamPromise 160 166 161 - await expect(streamPromise).rejects.toThrow() 167 + // Stream should complete after receiving the message 168 + expect(messages).toEqual(['msg1']) 162 169 }) 163 170 164 171 it('can be aborted with signal', async () => { ··· 171 178 } 172 179 })() 173 180 174 - server.send('msg1') 181 + MockServer.send(server, 'msg1') 175 182 176 183 setTimeout(() => { 177 184 controller.abort(new DOMException('Aborted', 'AbortError')) ··· 195 202 } 196 203 })() 197 204 198 - server.close() 205 + MockServer.close(server) 199 206 200 207 await streamPromise 201 208 ··· 219 226 } 220 227 })() 221 228 222 - server.send('{"id":1}') 223 - server.send('{"id":2}') 229 + MockServer.send(server, '{"id":1}') 230 + MockServer.send(server, '{"id":2}') 224 231 225 232 await streamPromise 226 233 ··· 239 246 } 240 247 })() 241 248 242 - server.send('{"id":"not a number"}') 249 + MockServer.send(server, '{"id":"not a number"}') 243 250 await expect(streamPromise).resolves.toBe(true) 244 251 }) 245 252 })
+1 -1
src/lib/socket.ts
··· 180 180 // TODO: eslint thinks inBackoffMode is always falsey... 181 181 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 182 182 if (inBackoffMode && queue.depth < backoffThresh) { 183 - console.debug('message stream will stop dropping messages due to eased backpressure') 183 + console.log('message stream will stop dropping messages due to eased backpressure') 184 184 inBackoffMode = false 185 185 } 186 186
+7 -7
src/lib/strict-map.spec.ts
··· 1 - import {describe, expect, it, jest} from '@jest/globals' 1 + import {describe, expect, it, vi} from 'vitest' 2 2 3 3 import {StrictMap} from '#lib/strict-map' 4 4 ··· 25 25 describe('ensure', () => { 26 26 it('returns existing value if key exists', () => { 27 27 const map = new StrictMap<string, number>() 28 - const maker = jest.fn(() => 999) 28 + const maker = vi.fn(() => 999) 29 29 map.set('key', 42) 30 30 31 31 const value = map.ensure('key', maker) ··· 36 36 37 37 it('creates and returns new value if key does not exist', () => { 38 38 const map = new StrictMap<string, number>() 39 - const maker = jest.fn(() => 42) 39 + const maker = vi.fn(() => 42) 40 40 41 41 const value = map.ensure('key', maker) 42 42 ··· 47 47 48 48 it('only calls maker once per key', () => { 49 49 const map = new StrictMap<string, number>() 50 - const maker = jest.fn(() => 42) 50 + const maker = vi.fn(() => 42) 51 51 52 52 map.ensure('key', maker) 53 53 map.ensure('key', maker) ··· 59 59 describe('ensureAsync', () => { 60 60 it('returns existing value if key exists', async () => { 61 61 const map = new StrictMap<string, number>() 62 - const maker = jest.fn(() => Promise.resolve(999)) 62 + const maker = vi.fn(() => Promise.resolve(999)) 63 63 map.set('key', 42) 64 64 65 65 const value = await map.ensureAsync('key', maker) ··· 70 70 71 71 it('creates and returns new value if key does not exist', async () => { 72 72 const map = new StrictMap<string, number>() 73 - const maker = jest.fn(() => Promise.resolve(42)) 73 + const maker = vi.fn(() => Promise.resolve(42)) 74 74 75 75 const value = await map.ensureAsync('key', maker) 76 76 ··· 82 82 it('handles race conditions', async () => { 83 83 const map = new StrictMap<string, number>() 84 84 let callCount = 0 85 - const maker = jest.fn(async () => { 85 + const maker = vi.fn(async () => { 86 86 callCount++ 87 87 await sleep(10) 88 88 return callCount
+15 -1
src/realm/client/action-store.ts
··· 1 1 import Dexie, {Collection, Table} from 'dexie' 2 2 3 + import {normalizeError} from '#lib/errors' 3 4 import {TypedEventTarget} from '#lib/events' 4 5 5 6 import {LogicalClock} from '#realm/logical-clock' ··· 59 60 [[] as StoredAction[], {} as PeerClocks], 60 61 ) 61 62 63 + console.log('recording actions and clocks', actions, actionrows, clocks) 64 + 62 65 let addedkeys: Timestamp[] | undefined 63 66 await this.transaction('rw', ['actions', 'clocks'], async () => { 64 - addedkeys = await this.actions.bulkAdd(actionrows, {allKeys: true}) 67 + try { 68 + addedkeys = await this.actions.bulkAdd(actionrows, {allKeys: true}) 69 + } catch (exc: unknown) { 70 + const error = normalizeError(exc) 71 + if (error.name !== 'BulkError') { 72 + throw error 73 + } 74 + 75 + // from dexie docs: If some operations fail, bulkAdd will ignore those failures but return a rejected 76 + // Promise with a Dexie.BulkError referencing failures. If caller does not catch that error, 77 + // transaction will abort. 78 + } 65 79 66 80 for (const [_identid, clock] of Object.entries(clocks)) { 67 81 if (clock === null) continue
+619
src/realm/client/connection.spec.ts
··· 1 + import {nanoid} from 'nanoid' 2 + import {beforeEach, describe, expect, it, vi} from 'vitest' 3 + 4 + import {MockDataChannel, MockSocket, createChannelPair, createSocketPair} from '#spec/helpers-socket-pair' 5 + 6 + import {sleep} from '#lib/async/sleep' 7 + import {DataChannelPeer} from '#lib/client/webrtc' 8 + import {generateSignableJwt, generateSigningJwkPair, jwkExport} from '#lib/crypto/jwks' 9 + import {jwtPayload} from '#lib/crypto/jwts' 10 + 11 + import { 12 + PreauthErrorResponse, 13 + preauthAuthnReqSchema, 14 + preauthExchangeInviteReqSchema, 15 + preauthRegisterReqSchema, 16 + preauthReqSchema, 17 + } from '#realm/protocol/messages-preauth' 18 + import {RealmPeerJoinedEvent, RealmPingEvent} from '#realm/protocol/messages-realm' 19 + import {MockActionStore} from '#realm/protocol/spec/mock-action-store' 20 + import {RealmAction, RealmAddress} from '#realm/schema' 21 + import {IdentBrand, RealmBrand} from '#realm/schema/brands' 22 + import {generate as generateTimestamp} from '#realm/schema/timestamp' 23 + 24 + import {RealmConnection} from './connection' 25 + 26 + describe('RealmConnection', () => { 27 + let identid: ReturnType<typeof IdentBrand.generate> 28 + let realmid: ReturnType<typeof RealmBrand.generate> 29 + let keypair: CryptoKeyPair 30 + let actions: MockActionStore 31 + let address: RealmAddress 32 + 33 + beforeEach(async () => { 34 + identid = IdentBrand.generate() 35 + realmid = RealmBrand.generate() 36 + keypair = await generateSigningJwkPair() 37 + actions = new MockActionStore() 38 + address = { 39 + realmid, 40 + upstream: 'wss://test.example.com', 41 + } 42 + }) 43 + 44 + describe('constructor', () => { 45 + it('creates connection with required parameters', () => { 46 + const connection = new RealmConnection(address, identid, keypair, actions as any) 47 + 48 + expect(connection.address).toBe(address) 49 + expect(connection.connected).toBe(false) 50 + expect(connection.peers).toEqual([]) 51 + 52 + connection.destroy() 53 + }) 54 + 55 + it('uses custom socket factory if provided', () => { 56 + const mockFactory = vi.fn(() => new MockSocket() as any) 57 + const connection = new RealmConnection(address, identid, keypair, actions as any, mockFactory) 58 + 59 + // Factory should be called after a short delay for connection 60 + setTimeout(() => { 61 + expect(mockFactory).toHaveBeenCalledWith(address.upstream) 62 + connection.destroy() 63 + }, 100) 64 + }) 65 + 66 + it('uses custom peer factory if provided', () => { 67 + const mockPeerFactory = vi.fn(() => new MockDataChannel(true) as any) 68 + 69 + const connection = new RealmConnection(address, identid, keypair, actions as any, undefined, { 70 + factory: mockPeerFactory, 71 + }) 72 + 73 + expect(connection).toBeDefined() 74 + connection.destroy() 75 + }) 76 + 77 + it('respects abort signal from constructor', async () => { 78 + const controller = new AbortController() 79 + const connection = new RealmConnection( 80 + address, 81 + identid, 82 + keypair, 83 + actions as any, 84 + undefined, 85 + undefined, 86 + controller.signal, 87 + ) 88 + 89 + controller.abort() 90 + await sleep(10) 91 + 92 + // Connection should not attempt to connect 93 + expect(connection.connected).toBe(false) 94 + connection.destroy() 95 + }) 96 + }) 97 + 98 + describe('connection lifecycle', () => { 99 + it('attempts to connect on creation', async () => { 100 + const [client] = createSocketPair() 101 + const socketFactory = vi.fn(() => client as any) 102 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 103 + 104 + // Should create socket shortly after construction 105 + await sleep(10) 106 + expect(socketFactory).toHaveBeenCalledWith(address.upstream) 107 + 108 + connection.destroy() 109 + }) 110 + 111 + it('handles socket open event', async () => { 112 + const [client] = createSocketPair(false) 113 + const socketFactory = () => client as any 114 + 115 + const openEvents: any[] = [] 116 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 117 + 118 + connection.addEventListener('wsopen', (e) => { 119 + openEvents.push(e.detail) 120 + }) 121 + 122 + // wait for socket to be created 123 + client.open() 124 + await sleep(100) 125 + 126 + expect(client.readyState).toBe(client.OPEN) 127 + expect(openEvents).toHaveLength(1) 128 + 129 + connection.destroy() 130 + }) 131 + 132 + it('handles socket close event', async () => { 133 + const [client] = createSocketPair() 134 + const socketFactory = () => client as any 135 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 136 + 137 + const closeEvents: any[] = [] 138 + connection.addEventListener('wsclose', () => { 139 + closeEvents.push(true) 140 + }) 141 + 142 + await sleep(10) 143 + client.close() 144 + await sleep(10) 145 + 146 + expect(closeEvents).toHaveLength(1) 147 + expect(connection.connected).toBe(false) 148 + 149 + connection.destroy() 150 + }) 151 + 152 + it('handles socket error event', async () => { 153 + const [client] = createSocketPair() 154 + const socketFactory = () => client as any 155 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 156 + 157 + const errorEvents: any[] = [] 158 + connection.addEventListener('wserror', (e) => { 159 + errorEvents.push(e.detail) 160 + }) 161 + 162 + await sleep(10) 163 + client.emit('error', new Error('test error')) 164 + await sleep(10) 165 + 166 + expect(errorEvents).toHaveLength(1) 167 + 168 + connection.destroy() 169 + }) 170 + 171 + it('reconnects with exponential backoff after disconnect', async () => { 172 + const sockets: MockSocket[] = [] 173 + const socketFactory = vi.fn(() => { 174 + const [client] = createSocketPair() 175 + sockets.push(client) 176 + 177 + // close socket immediately to trigger reconnect 178 + setTimeout(() => { 179 + client.close() 180 + }, 5) 181 + return client as any 182 + }) 183 + 184 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 185 + await sleep(0) 186 + expect(socketFactory).toHaveBeenCalledTimes(1) 187 + 188 + // wait for first reconnection attempts (immediate on failure) 189 + await sleep(10) 190 + expect(socketFactory).toHaveBeenCalledTimes(2) 191 + 192 + // wait for backoff and reconnect (backoff starts at 1000ms) 193 + await sleep(1100) 194 + expect(socketFactory).toHaveBeenCalledTimes(3) 195 + 196 + connection.destroy() 197 + }) 198 + }, 5000) 199 + 200 + describe('authentication', () => { 201 + it('sends authentication request on connect', async () => { 202 + const [client, server] = createSocketPair() 203 + const socketFactory = () => client as any 204 + 205 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 206 + 207 + // Wait for connection and auth attempt 208 + await sleep(50) 209 + 210 + // Should have sent auth message 211 + const authMessage = await server.nextMessage() 212 + expect(authMessage).toBeDefined() 213 + 214 + // Verify it's a valid JWT 215 + const parts = authMessage.split('.') 216 + expect(parts).toHaveLength(3) 217 + 218 + connection.destroy() 219 + }) 220 + 221 + it('handles registration with public key', async () => { 222 + const [client, server] = createSocketPair() 223 + const socketFactory = () => client as any 224 + 225 + const registerAddress = { 226 + ...address, 227 + register: true, 228 + } 229 + 230 + const connection = new RealmConnection(registerAddress, identid, keypair, actions as any, socketFactory) 231 + await sleep(50) 232 + 233 + const _authRequest = await server.nextMessage() // consume auth request 234 + const authRequest = await jwtPayload(preauthRegisterReqSchema).safeParseAsync(_authRequest) 235 + 236 + expect(authRequest.success).toBe(true) 237 + expect(authRequest.data?.payload.dat.pubkey).not.toBe(undefined) 238 + 239 + connection.destroy() 240 + }) 241 + 242 + it('handles invitation exchange', async () => { 243 + const [client, server] = createSocketPair(false) 244 + const socketFactory = () => client as any 245 + 246 + const invitation = await generateSignableJwt({ 247 + aud: RealmBrand.generate(), 248 + iss: IdentBrand.generate(), 249 + jti: nanoid(), 250 + exp: Date.now() / 1000 + 60, 251 + }).sign(keypair.privateKey) 252 + const inviteAddress = {...address, invitation} 253 + const connection = new RealmConnection(inviteAddress, identid, keypair, actions as any, socketFactory) 254 + 255 + server.open() 256 + client.open() 257 + await sleep(50) 258 + 259 + const _authRequest = await server.nextMessage() // consume auth request 260 + const authRequest = await jwtPayload(preauthExchangeInviteReqSchema).safeParseAsync(_authRequest) 261 + 262 + expect(authRequest.data?.payload.msg).toBe('preauth.exchange') 263 + expect(authRequest.data?.payload.dat).toHaveProperty('inviteJwt') 264 + expect(authRequest.data?.payload.dat.inviteJwt).toBe(invitation) 265 + 266 + connection.destroy() 267 + }) 268 + 269 + it('handles successful authentication response', async () => { 270 + const [client, server] = createSocketPair() 271 + const socketFactory = () => client as any 272 + const peerConfig = { 273 + factory: (initiator: boolean) => new MockDataChannel(initiator) as unknown as DataChannelPeer, 274 + } 275 + 276 + const connection = new RealmConnection( 277 + address, 278 + identid, 279 + keypair, 280 + actions as any, 281 + socketFactory, 282 + peerConfig, 283 + ) 284 + await sleep(50) 285 + 286 + const _authRequest = await server.nextMessage() // consume auth request 287 + const authRequest = await jwtPayload(preauthAuthnReqSchema).safeParseAsync(_authRequest) 288 + 289 + // Send success response 290 + const otherIdentid = IdentBrand.generate() 291 + const otherKeypair = await generateSigningJwkPair() 292 + const otherPubkey = await jwkExport.parseAsync(otherKeypair.publicKey) 293 + const successResponse = { 294 + typ: 'res', 295 + msg: authRequest.data?.payload.msg, 296 + seq: authRequest.data?.payload.seq, 297 + dat: { 298 + identities: { 299 + [otherIdentid]: otherPubkey, 300 + }, 301 + peers: [otherIdentid], 302 + }, 303 + } 304 + 305 + server.send(JSON.stringify(successResponse)) 306 + await sleep(50) 307 + 308 + expect(connection.connected).toBe(true) 309 + expect(connection.peers).toContain(otherIdentid) 310 + 311 + connection.destroy() 312 + }) 313 + 314 + it('handles authentication error response', async () => { 315 + const [client, server] = createSocketPair() 316 + const socketFactory = () => client as any 317 + 318 + const errorEvents: any[] = [] 319 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 320 + 321 + connection.addEventListener('error', (e) => { 322 + errorEvents.push(e.detail) 323 + }) 324 + 325 + await sleep(50) 326 + 327 + // parse the JWT to get the seq field 328 + const _authRequest = await server.nextMessage() // consume auth request 329 + const authRequest = await jwtPayload(preauthAuthnReqSchema).safeParseAsync(_authRequest) 330 + expect(authRequest.success).toBe(true) 331 + 332 + // Send error response 333 + const errorResponse = { 334 + typ: 'err', 335 + msg: 'preauth.authn', 336 + seq: authRequest.data!.payload.seq, 337 + err: { 338 + code: 401, 339 + detail: 'Invalid credentials', 340 + }, 341 + } satisfies PreauthErrorResponse 342 + 343 + server.send(JSON.stringify(errorResponse)) 344 + await sleep(50) 345 + 346 + expect(connection.connected).toBe(false) 347 + expect(errorEvents).toHaveLength(1) 348 + 349 + connection.destroy() 350 + }) 351 + }) 352 + 353 + describe('message handling', () => { 354 + async function setupAuthenticatedConnection() { 355 + const [client, server] = createSocketPair() 356 + const socketFactory = () => client as any 357 + const peerConfig = { 358 + factory: (initiator: boolean) => new MockDataChannel(initiator) as unknown as DataChannelPeer, 359 + } 360 + 361 + const connection = new RealmConnection( 362 + address, 363 + identid, 364 + keypair, 365 + actions as any, 366 + socketFactory, 367 + peerConfig, 368 + ) 369 + 370 + await sleep(50) 371 + const _authRequest = await server.nextMessage() // consume auth request 372 + expect(_authRequest).not.toBe(undefined) 373 + 374 + // send success response with no peers 375 + const authRequest = await jwtPayload(preauthAuthnReqSchema).parseAsync(_authRequest) 376 + const successResponse = { 377 + typ: 'res', 378 + msg: 'preauth.authn', 379 + seq: authRequest.payload.seq, // Include the seq from the request 380 + dat: { 381 + identities: {}, 382 + peers: [], 383 + }, 384 + } 385 + 386 + server.send(JSON.stringify(successResponse)) 387 + await sleep(50) 388 + 389 + return {connection, client, server} 390 + } 391 + 392 + it('handles peer-joined message', async () => { 393 + const {connection, server} = await setupAuthenticatedConnection() 394 + 395 + const joinEvents: any[] = [] 396 + connection.addEventListener('peerjoined', (e) => { 397 + joinEvents.push(e.detail) 398 + }) 399 + 400 + const newPeerId = IdentBrand.generate() 401 + const newPeerKeypair = await generateSigningJwkPair() 402 + const newPeerPubkey = await jwkExport.parseAsync(newPeerKeypair.publicKey) 403 + const peerJoinedMessage = { 404 + typ: 'evt', 405 + msg: 'realm.peer-joined', 406 + dat: { 407 + identid: newPeerId, 408 + pubkey: newPeerPubkey, 409 + }, 410 + } satisfies RealmPeerJoinedEvent 411 + 412 + server.send(JSON.stringify(peerJoinedMessage)) 413 + await sleep(50) 414 + 415 + expect(joinEvents).toHaveLength(1) 416 + expect(joinEvents[0]).toEqual({identid: newPeerId}) 417 + expect(connection.peers).toContain(newPeerId) 418 + 419 + connection.destroy() 420 + }) 421 + 422 + it('handles peer-left message', async () => { 423 + const {connection, server} = await setupAuthenticatedConnection() 424 + 425 + const leftEvents: any[] = [] 426 + connection.addEventListener('peerleft', (e) => { 427 + leftEvents.push(e.detail) 428 + }) 429 + 430 + const peerId = IdentBrand.generate() 431 + const peerLeftMessage = { 432 + typ: 'evt', 433 + msg: 'realm.peer-left', 434 + dat: { 435 + identid: peerId, 436 + }, 437 + } 438 + 439 + server.send(JSON.stringify(peerLeftMessage)) 440 + await sleep(50) 441 + 442 + expect(leftEvents).toHaveLength(1) 443 + expect(leftEvents[0]).toEqual({identid: peerId}) 444 + 445 + connection.destroy() 446 + }) 447 + 448 + it('handles ping message with sync request', async () => { 449 + const {connection, server} = await setupAuthenticatedConnection() 450 + await server.nextMessage() 451 + 452 + // add some actions to the store 453 + actions.addAction({ 454 + typ: 'act' as const, 455 + msg: 'test.action', 456 + clk: generateTimestamp(identid, 1000, 0), 457 + dat: {test: true}, 458 + } satisfies RealmAction) 459 + 460 + const pingMessage = { 461 + typ: 'evt', 462 + msg: 'realm.ping', 463 + dat: { 464 + clocks: {}, 465 + requestSync: true, 466 + }, 467 + } satisfies RealmPingEvent 468 + 469 + server.send(JSON.stringify(pingMessage)) 470 + await sleep(50) 471 + 472 + // Should receive sync delta 473 + const response = await server.nextMessage() 474 + console.log('response:', response) 475 + 476 + const parsed = JSON.parse(response) 477 + expect(Array.isArray(parsed)).toBe(true) 478 + expect(parsed).toHaveLength(1) 479 + 480 + connection.destroy() 481 + }) 482 + 483 + it('dispatches wsdata event for unparseable messages', async () => { 484 + const {connection, server} = await setupAuthenticatedConnection() 485 + 486 + const dataEvents: any[] = [] 487 + connection.addEventListener('wsdata', (e) => { 488 + dataEvents.push(e.detail) 489 + }) 490 + 491 + server.send('not json') 492 + await sleep(50) 493 + 494 + expect(dataEvents).toHaveLength(1) 495 + expect(dataEvents[0]).toBe('not json') 496 + 497 + connection.destroy() 498 + }) 499 + }) 500 + 501 + describe('broadcast', () => { 502 + async function setupConnectionWithPeer() { 503 + const [client, server] = createSocketPair() 504 + const [peerChan1, peerChan2] = createChannelPair(true) 505 + const socketFactory = () => client as any 506 + const peerFactory = () => peerChan1 as any 507 + 508 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory, { 509 + factory: peerFactory, 510 + }) 511 + const _authRequest = await server.nextMessage() // consume auth request 512 + const authRequest = await jwtPayload(preauthReqSchema).parseAsync(_authRequest) 513 + 514 + const otherIdentid = IdentBrand.generate() 515 + const otherKeypair = await generateSigningJwkPair() 516 + const otherPubkey = await jwkExport.parseAsync(otherKeypair.publicKey) 517 + 518 + // Send success response with a peer 519 + const successResponse = { 520 + typ: 'res', 521 + msg: authRequest.payload.msg, 522 + seq: authRequest.payload.seq, 523 + dat: { 524 + identities: { 525 + [otherIdentid]: otherPubkey, 526 + }, 527 + peers: [otherIdentid], 528 + }, 529 + } 530 + 531 + server.send(JSON.stringify(successResponse)) 532 + await sleep(100) 533 + 534 + return {connection, client, server, peerChan1, peerChan2, otherIdentid} 535 + } 536 + 537 + it('sends to all connected peers', async () => { 538 + const {connection, peerChan2} = await setupConnectionWithPeer() 539 + 540 + const payload = {test: 'data'} 541 + connection.broadcast(payload) 542 + 543 + const message = await peerChan2.nextMessage() 544 + expect(JSON.parse(message as string)).toEqual(payload) 545 + 546 + connection.destroy() 547 + }, 1000) 548 + 549 + it('queues for server when connected', async () => { 550 + const {connection, server} = await setupConnectionWithPeer() 551 + await server.nextMessage() // purge initial ping 552 + 553 + const payload = {test: 'server data'} 554 + connection.broadcast(payload) 555 + 556 + await sleep(50) 557 + const message = await server.nextMessage() 558 + expect(JSON.parse(message)).toEqual(payload) 559 + 560 + connection.destroy() 561 + }, 1000) 562 + 563 + it('skips server queue when not ready', () => { 564 + const connection = new RealmConnection(address, identid, keypair, actions as any) 565 + 566 + // Broadcast before connection is ready 567 + const payload = {test: 'data'} 568 + expect(() => { 569 + connection.broadcast(payload) 570 + }).not.toThrow() 571 + 572 + connection.destroy() 573 + }, 1000) 574 + }) 575 + 576 + describe('peer management', () => { 577 + it('determines initiator based on identity comparison', () => { 578 + const connection = new RealmConnection(address, identid, keypair, actions as any) 579 + 580 + const peerId1 = IdentBrand.parse('idt.aaaaaaaaaaaaaaaa') 581 + const peerId2 = IdentBrand.parse('idt.zzzzzzzzzzzzzzzz') 582 + 583 + // our identid compared to peers 584 + const shouldInitiate1 = connection.shouldInitiatePeer(peerId1) 585 + const shouldInitiate2 = connection.shouldInitiatePeer(peerId2) 586 + 587 + // one should be true, one false (depends on our generated identid) 588 + expect([shouldInitiate1, shouldInitiate2]).toContain(true) 589 + expect([shouldInitiate1, shouldInitiate2]).toContain(false) 590 + 591 + connection.destroy() 592 + }) 593 + }) 594 + 595 + describe('cleanup', () => { 596 + it('destroys cleanly when called', async () => { 597 + const [client] = createSocketPair() 598 + const socketFactory = () => client as any 599 + 600 + const connection = new RealmConnection(address, identid, keypair, actions as any, socketFactory) 601 + 602 + await sleep(10) 603 + connection.destroy() 604 + await sleep(10) 605 + 606 + expect(client.readyState).toBe(client.CLOSED) 607 + }) 608 + 609 + it('handles multiple destroy calls', () => { 610 + const connection = new RealmConnection(address, identid, keypair, actions as any) 611 + 612 + expect(() => { 613 + connection.destroy() 614 + connection.destroy() 615 + connection.destroy() 616 + }).not.toThrow() 617 + }) 618 + }) 619 + })
+111 -179
src/realm/client/connection.ts
··· 30 30 } from '#realm/protocol/messages-preauth' 31 31 import { 32 32 RealmPingEvent, 33 + RealmSignalEvent, 33 34 realmPeerJoinedEventSchema, 34 35 realmPeerLeftEventSchema, 35 36 realmPingEventSchema, ··· 39 40 import {IdentBrand, IdentID} from '#realm/schema/brands' 40 41 41 42 import {ActionStore} from './action-store' 43 + import {RealmPeer} from './realm-peer' 42 44 43 - const realmPeerMessageSchema = z.union([realmPingEventSchema]) 44 45 const realmSocketMessageSchema = z.union([ 45 46 realmSignalEventSchema, 46 47 realmPingEventSchema, ··· 61 62 peerleft: CustomEvent<{identid: IdentID}> 62 63 } 63 64 64 - type RealmPeerEventMap = DataChannelEventMap & { 65 + export type RealmPeerEventMap = DataChannelEventMap & { 65 66 peeropen: CustomEvent<{identid: IdentID}> 66 67 peerclose: CustomEvent<{identid: IdentID}> 67 68 peererror: CustomEvent<{identid: IdentID; error: Error}> ··· 70 71 71 72 const SOCKET_HIGHWATER = 1024 * 1024 // 1Mb 72 73 74 + export type RealmSocketFactory = (url: string) => WebSocket 75 + export type RealmPeerFactory = ( 76 + initiator: boolean, 77 + config?: RTCConfiguration, 78 + signal?: AbortSignal, 79 + ) => DataChannelPeer 80 + export type RealmPeerConfiguration = Partial<RTCConfiguration> & {factory?: RealmPeerFactory} 81 + 73 82 /** 74 83 * a realm connection is the persistent connection to a signalling realm server and a collection of peers 75 84 * ··· 107 116 #actions: ActionStore 108 117 109 118 #identities = new StrictMap<IdentID, CryptoKey>() 110 - #peers = new StrictMap<IdentID, RealmPeer>() 111 - #peerConfig: Partial<RTCConfiguration> | undefined 112 119 113 120 #socket: WebSocket | null = null 121 + #socketFactory: RealmSocketFactory 114 122 #socketQueue = new BlockingQueue<string>() 115 123 #socketTimeout: TimeoutSignal | null = null 116 124 #socketAbort: AbortController | null = null 117 125 126 + #peers = new StrictMap<IdentID, RealmPeer>() 127 + #peerConfig: Partial<RTCConfiguration> 128 + #peerFactory: RealmPeerFactory 129 + 118 130 constructor( 119 131 address: RealmAddress, 120 132 identid: IdentID, 121 133 keypair: CryptoKeyPair, 122 134 actions: ActionStore, 123 - config?: Partial<RTCConfiguration>, 135 + socketFactory?: RealmSocketFactory, 136 + peerConfig?: RealmPeerConfiguration, 124 137 signal?: AbortSignal, 125 138 ) { 126 139 super() ··· 131 144 this.#keypair = keypair 132 145 this.#actions = actions 133 146 134 - this.#peerConfig = config 147 + this.#socketFactory = socketFactory || ((u: string) => new WebSocket(u)) 148 + 149 + const {factory, ...config} = peerConfig || {} 150 + this.#peerFactory = factory || ((i, c, s) => new DataChannelPeer(i, c, s)) 151 + this.#peerConfig = {...config} 135 152 136 153 this.#connectLoop().catch((exc: unknown) => { 154 + if (this.#abort.signal.aborted) return 155 + 137 156 console.error('fatal error in connection loop', exc) 138 157 this.dispatchCustomEvent('error', normalizeError(exc)) 139 158 }) ··· 147 166 return this.#ready.value 148 167 } 149 168 169 + get peers(): ReadonlyArray<IdentID> { 170 + return Array.from(this.#peers.keys()) 171 + } 172 + 150 173 async #connectLoop() { 151 174 const delay = backoff({baseDelay: 1000, maxDelay: 30_000}) 152 175 while (!this.#abort.signal.aborted) { 153 176 try { 154 177 await this.#connectedSocket(this.#abort.signal) 178 + } catch (exc: unknown) { 179 + if (this.#abort.signal.aborted) return 155 180 156 - console.log('reconnecting', delay.attempts, 'after', delay.delay) 157 - await delay(this.#abort.signal) 158 - } catch (exc: unknown) { 159 - console.error('unexpected error in connect loop, continuing...', exc) 160 - this.dispatchCustomEvent('error', normalizeError(exc)) 161 - await delay(this.#abort.signal) 181 + const error = normalizeError(exc) 182 + console.warn('unexpected error in connect loop, continuing...', error.name, error.message) 183 + 184 + this.dispatchCustomEvent('error', error) 162 185 } 186 + 187 + console.info('waiting %dms for reconnect, connection #%d', delay.delay, delay.attempts) 188 + await delay(this.#abort.signal) 163 189 } 164 190 } 165 191 ··· 172 198 173 199 try { 174 200 const opts = {signal: this.#socketAbort.signal, once: true} 175 - this.#socket = new WebSocket(this.#address.upstream) 201 + this.#socket = this.#socketFactory(this.#address.upstream) 176 202 177 203 this.#socket.addEventListener( 178 204 'open', ··· 217 243 } 218 244 219 245 broadcast<T>(payload: T) { 220 - if (!this.#ready.value) return 221 - 222 246 const json = JSON.stringify(payload) 223 247 const recipients = Array.of(...this.#peers.keys()) 224 248 ··· 228 252 }) 229 253 230 254 // broadcast through to the server so that it can store actions 231 - this.#socketQueue.enqueue(json) 255 + if (this.#ready.value) { 256 + this.#socketQueue.enqueue(json) 257 + } 232 258 } 233 259 234 260 destroy() { ··· 250 276 this.#peers.clear() 251 277 } 252 278 253 - chooseSyncPeer(): IdentID | null { 254 - let max: IdentID | null = null 255 - for (const key of this.#peers.keys()) { 256 - if (key === this.#identid) continue 257 - if (max === null || max.localeCompare(key) < 0) { 258 - max = key 259 - } 260 - } 261 - 262 - return max 279 + shouldInitiatePeer(peerid: IdentID): boolean { 280 + return this.#identid.localeCompare(peerid) < 0 263 281 } 264 282 265 283 /// ··· 267 285 #socketOnOpen = async () => { 268 286 this.dispatchCustomEvent('wsopen', this.#address) 269 287 270 - // drain loop is special, it doesn't wait for ready 271 - this.#socketDrainLoop().catch((exc: unknown) => { 272 - console.error('uncaught error in socket drain loop', exc) 273 - }) 274 - 275 288 try { 276 289 const authresp = await this.#socketAuthenticate() 277 290 if (authresp.typ === 'err') { 278 291 this.#ready.close() 279 - throw new ProtocolError(authresp.err.detail ?? 'unknown error', authresp.err.code) 292 + const error = new ProtocolError(authresp.err.detail ?? 'unknown error', authresp.err.code) 293 + 294 + console.error('error in authenticate!', error) 295 + this.dispatchCustomEvent('error', error) 296 + return 280 297 } 281 298 282 299 this.#address = { ··· 302 319 // initiate outbound connections when starting up 303 320 for (const peerid of authresp.dat.peers) { 304 321 if (peerid === this.#identid) continue 305 - this.#peerConnect(peerid, true) 322 + 323 + // initiate deterministically 324 + this.#peerConnect(peerid, this.shouldInitiatePeer(peerid)) 306 325 } 307 326 308 327 // we're ready! 309 328 this.#ready.open() 310 329 311 330 // start the loops 331 + this.#socketDrainLoop().catch((exc: unknown) => { 332 + if (this.#abort.signal.aborted) return 333 + console.error('uncaught error in socket drain loop', exc) 334 + }) 335 + 312 336 this.#socketMessageLoop().catch((exc: unknown) => { 313 - console.error('uncaught error in message loop', exc) 337 + if (this.#abort.signal.aborted) return 338 + console.error('uncaught error in socket message loop', exc) 314 339 }) 315 340 316 341 this.#socketPingLoop().catch((exc: unknown) => { 317 - console.error('uncaught error in ping loop', exc) 342 + if (this.#abort.signal.aborted) return 343 + console.error('uncaught error in socket ping loop', exc) 318 344 }) 319 345 } catch (exc: unknown) { 320 - console.warn('unknown error in socket outhentication or setup', exc) 346 + const error = normalizeError(exc) 347 + console.warn('unknown error in socket authentication or setup', error.name, error.message) 348 + 321 349 this.#socket?.close() 322 - this.dispatchCustomEvent('error', normalizeError(exc)) 323 - throw exc 350 + this.dispatchCustomEvent('error', error) 351 + throw error 324 352 } 325 353 } 326 354 ··· 347 375 #socketMessageLoop = async () => { 348 376 await this.#ready.enter(this.#socketAbort?.signal) 349 377 350 - const stream = streamSocket(this.#socket as unknown as ISOWS, {signal: this.#socketAbort?.signal}) 378 + const socket = this.#socket as ISOWS | null 379 + if (!socket) { 380 + console.warn('after ready, but socket is null?') 381 + 382 + this.dispatchCustomEvent('error', new Error('after ready, socket is null')) 383 + return 384 + } 385 + 386 + const stream = streamSocket(socket, {signal: this.#socketAbort?.signal}) 351 387 try { 352 388 for await (const raw of stream) { 353 389 await this.#socketOnData(raw) 354 390 } 355 391 } catch (exc: unknown) { 356 - console.error('socket stream error', exc) 392 + if (this.#abort.signal.aborted) return 393 + const error = normalizeError(exc) 394 + console.warn('socket stream error', error.name, error.message) 395 + 357 396 this.dispatchCustomEvent('error', normalizeError(exc)) 358 397 } 359 398 } 360 399 361 400 #socketDrainLoop = async () => { 401 + await this.#ready.enter(this.#socketAbort?.signal) 362 402 if (this.#socket == null) { 363 403 throw new Error('unexpectedly ready with no socket?') 364 404 } ··· 371 411 } 372 412 373 413 // channel closed while waiting? 374 - if (this.#socket.readyState !== WebSocket.OPEN) { 414 + if (!this.#ready.value) { 375 415 this.#socketQueue.prequeue(data) 376 416 break 377 417 } ··· 402 442 /// 403 443 404 444 #socketAuthenticate = async () => { 405 - let token: SignJWT 445 + const socket = this.#socket as ISOWS | null 446 + if (!socket) { 447 + console.warn('in socket authenticate, but socket is null?') 448 + throw new Error('after ready, socket is null') 449 + } 406 450 407 451 // send the appropriate type of request 452 + let token: SignJWT 408 453 if (this.#address.register) { 409 454 token = await this.#buildAuthenticateRegister() 410 455 } else if (this.#address.invitation) { ··· 414 459 } 415 460 416 461 const signed = await token.sign(this.#keypair.privateKey) 417 - this.#socketQueue.enqueue(signed) 462 + socket.send(signed) 418 463 419 464 // now, we're expecting an authenticated responese 420 465 const timeout = timeoutSignal(5000) 421 466 const signal = combineSignals(this.#socketAbort?.signal, timeout.signal) 422 - return await takeSocketSchema(this.#socket as unknown as ISOWS, jsonCodec(preauthResSchema), signal) 467 + return await takeSocketSchema(socket, jsonCodec(preauthResSchema), signal) 423 468 } 424 469 425 470 #buildAuthenticateAuthn = () => { ··· 471 516 #socketOnMessage = async (data: RealmSocketMessage) => { 472 517 switch (data.msg) { 473 518 case 'realm.peer-joined': 519 + this.dispatchCustomEvent('peerjoined', {identid: data.dat.identid}) 474 520 this.#identities.set(data.dat.identid, await jwkImport.parseAsync(data.dat.pubkey)) 475 - this.dispatchCustomEvent('peerjoined', {identid: data.dat.identid}) 521 + this.#peerConnect(data.dat.identid, this.shouldInitiatePeer(data.dat.identid)) 476 522 return 477 523 478 524 case 'realm.peer-left': ··· 504 550 const token = await jwtPayload(rtcSignalPayloadSchema).parseAsync(data.dat.signed) 505 551 await verifyJwtToken(data.dat.signed, pubkey) 506 552 507 - let peer = this.#peers.get(data.dat.localid) 508 - if (!peer && token.payload.initiator) { 509 - // false because _we_ are not the initiator, they are (they did initial outbound connections) 510 - peer = this.#peerConnect(data.dat.localid, false) 511 - } 553 + const peer = 554 + this.#peers.get(data.dat.localid) || this.#peerConnect(data.dat.localid, !token.payload.initiator) 512 555 513 - // might not have a connection yet (if we're waiting for an answer) 514 - peer?.signal(token.payload.signal) 556 + peer.signal(token.payload.signal) 515 557 return 516 558 } 517 559 } ··· 535 577 const opts = {signal} 536 578 537 579 try { 538 - const peer = new RealmPeer(this, this.#actions, remoteid, initiator, this.#peerConfig, signal) 580 + const chan = this.#peerFactory(initiator, this.#peerConfig, signal) 581 + const peer = new RealmPeer(this.#actions, remoteid, initiator, chan, signal) 582 + 539 583 this.#abort.signal.addEventListener('abort', () => { 540 584 timeout.cancel() 541 585 }) ··· 589 633 return peer 590 634 } catch (exc) { 591 635 timeout.cancel() 636 + console.error('error while setting up peer', exc) 592 637 throw exc 593 638 } 594 639 }) ··· 612 657 }) 613 658 614 659 const signed = await token.sign(this.#keypair.privateKey) 615 - this.#socketQueue.enqueue(signed) 660 + const signal: RealmSignalEvent = { 661 + typ: 'evt', 662 + msg: 'realm.signal', 663 + dat: { 664 + localid: this.#identid, 665 + remoteid: peer.identid, 666 + signed, 667 + }, 668 + } 669 + 670 + this.#socketQueue.enqueue(JSON.stringify(signal)) 616 671 } 617 672 618 673 go().catch((exc: unknown) => { 619 - console.error('error during peer signal send!', exc) 674 + const error = normalizeError(exc) 675 + console.error('error during peer signal send!', error.name, error.message) 620 676 this.dispatchCustomEvent('error', normalizeError(exc)) 621 677 }) 622 678 } 623 679 } 624 - 625 - export class RealmPeer extends DataChannelPeer<RealmPeerEventMap> { 626 - #abort: AbortController 627 - #connected: Fence 628 - #queue: BlockingQueue<string> 629 - 630 - constructor( 631 - readonly realm: RealmConnection, 632 - readonly actions: ActionStore, 633 - readonly identid: IdentID, 634 - readonly initiator: boolean, 635 - config?: Partial<RTCConfiguration>, 636 - signal?: AbortSignal, 637 - ) { 638 - super(initiator, { 639 - iceServers: [{urls: 'stun:stun.l.google.com:19302'}, {urls: 'stun:stun1.l.google.com:19302'}], 640 - iceCandidatePoolSize: 0, // no pre-gather, trickle ico 641 - bundlePolicy: 'balanced', 642 - iceTransportPolicy: 'all', 643 - ...(config || {}), 644 - }) 645 - 646 - this.#abort = controllerWithSignals(signal) 647 - this.#connected = new Fence() 648 - this.#queue = new BlockingQueue() 649 - 650 - const opts = {signal: this.#abort.signal} 651 - this.addEventListener('connect', this.#peerConnected, opts) 652 - this.addEventListener('close', this.#peerClosed, opts) 653 - this.addEventListener('data', this.#peerData, opts) 654 - 655 - this.#pingLoop().catch((exc: unknown) => { 656 - console.warn('unexpected error in ping loop', exc) 657 - }) 658 - 659 - this.#receiveLoop().catch((exc: unknown) => { 660 - console.warn('unexpected error in receive loop', exc) 661 - }) 662 - } 663 - 664 - destroy(error?: Error) { 665 - this.#abort.abort(error) 666 - super.destroy() 667 - } 668 - 669 - /// 670 - 671 - #peerConnected = () => { 672 - this.#connected.open() 673 - } 674 - 675 - #peerClosed = () => { 676 - this.#connected.close() 677 - } 678 - 679 - #peerData = (event: CustomEvent<string | ArrayBuffer>) => { 680 - const data = event.detail 681 - const string = typeof data === 'string' ? data : new TextDecoder().decode(data) 682 - this.#queue.enqueue(string) 683 - } 684 - 685 - /// 686 - 687 - #receiveLoop = async () => { 688 - this.#abort.signal.throwIfAborted() 689 - 690 - while (await this.#connected.enter(this.#abort.signal)) { 691 - const payload = await this.#queue.dequeue(this.#abort.signal) 692 - if (!this.#connected.value) break 693 - 694 - const parsed = await jsonCodec(realmPeerMessageSchema).safeParseAsync(payload) 695 - if (parsed.success) { 696 - await this.#receivePing(parsed.data) 697 - } else { 698 - this.dispatchCustomEvent('peerdata', {identid: this.identid, data: payload}) 699 - } 700 - } 701 - } 702 - 703 - #pingLoop = async () => { 704 - while (await this.#connected.enter(this.#abort.signal)) { 705 - // wait a moment to let the connect settle before ping 706 - await sleep(100, this.#abort.signal) 707 - if (!this.#connected.value) { 708 - break 709 - } 710 - 711 - await this.#sendPing() 712 - await sleep(30_000, this.#abort.signal) 713 - } 714 - } 715 - 716 - #sendPing = async () => { 717 - if (!this.#connected.value) return 718 - 719 - const clocks = await this.actions.buildSyncState() 720 - const syncPeer = this.realm.chooseSyncPeer() 721 - 722 - const message: RealmPingEvent = { 723 - typ: 'evt', 724 - msg: 'realm.ping', 725 - dat: {clocks, requestSync: syncPeer === this.identid}, 726 - } 727 - 728 - this.send(JSON.stringify(message)) 729 - } 730 - 731 - #receivePing = async (ping: RealmPingEvent) => { 732 - if (!ping.dat.requestSync) return 733 - 734 - // TODO: record last received ping 735 - 736 - this.#sendActions(await this.actions.buildSyncDelta(ping.dat.clocks)) 737 - } 738 - 739 - /// 740 - 741 - #sendActions(actions: RealmAction[], batch_size = 100) { 742 - for (let i = 0; i < actions.length; i += batch_size) { 743 - const batch = actions.slice(i, i + batch_size) 744 - this.send(JSON.stringify(batch)) 745 - } 746 - } 747 - }
+11 -2
src/realm/client/identity-store.ts
··· 32 32 return keys.map((k) => IdentBrand.parse(k)) 33 33 } 34 34 35 - async load(identid: IdentID): Promise<RealmIdentity | undefined> { 35 + async load(identid: IdentID): Promise<RealmIdentity> { 36 36 const row = await this.identities.get(identid) 37 - return row ? this.#loadIdentity(row) : undefined 37 + if (!row) { 38 + throw new Error('identity not found!') 39 + } 40 + 41 + return this.#loadIdentity(row) 38 42 } 39 43 40 44 ensure(): Promise<RealmIdentity> { ··· 52 56 53 57 return this.#loadIdentity(row) 54 58 }) 59 + } 60 + 61 + bootstrap(identid?: IdentID) { 62 + return identid ? this.load(identid) : this.ensure() 55 63 } 56 64 57 65 #loadIdentity(row: StoredIdentity) { ··· 83 91 } 84 92 85 93 #onTick = (identity: RealmIdentity, event: CustomEvent<Timestamp>) => { 94 + console.log('updating tick!', identity, event.detail) 86 95 this.identities.update(identity.identid, {clock: event.detail}).catch((exc: unknown) => { 87 96 console.error('uncaught error in identity tick handler', exc) 88 97 })
+39 -9
src/realm/client/identity.ts
··· 1 + import {nanoid} from 'nanoid' 1 2 import {z} from 'zod/v4' 2 3 3 4 import {controllerWithSignals} from '#lib/async/aborts' 5 + import {generateSignableJwt} from '#lib/crypto/jwks' 4 6 import {jwtSchema} from '#lib/crypto/jwts' 5 7 import {TypedEventTarget} from '#lib/events' 6 8 import {jsonCodec} from '#lib/schema' ··· 20 22 21 23 tick: CustomEvent<Timestamp> 22 24 action: CustomEvent<RealmAction> 25 + data: CustomEvent<unknown> 23 26 24 - peerjoined: CustomEvent<{identid: IdentID}> 25 - peerleft: CustomEvent<{identid: IdentID}> 27 + peeropen: CustomEvent<{identid: IdentID}> 28 + peerclose: CustomEvent<{identid: IdentID}> 26 29 } 27 30 28 31 const realmIdentityMessageSchema = z.union([ 29 32 z.array(actionSchema), 30 - // other messages (for now) are handled by connection directly 33 + // other messages (for now) are handled upstream 31 34 ]) 32 35 33 36 export class RealmIdentity extends TypedEventTarget<RealmIdentityEventMap> { ··· 78 81 79 82 this.#connectionAbort = controllerWithSignals(this.#abort.signal) 80 83 const opts = {signal: this.#connectionAbort.signal} 81 - const dispatch = this.dispatchEvent.bind(this) as EventListener 82 84 83 85 this.#connection = new RealmConnection( 84 86 remote, 85 87 this.identid, 86 88 this.keypair, 87 89 this.actions, 88 - {}, 90 + undefined, // socketFactory 91 + undefined, // peerConfig 89 92 this.#abort.signal, 90 93 ) 91 94 this.#connection.addEventListener('wsopen', this.#onWSOpen, opts) 92 95 this.#connection.addEventListener('wsclose', this.#onWSClose, opts) 93 - this.#connection.addEventListener('peerjoined', dispatch, opts) 94 - this.#connection.addEventListener('peerleft', dispatch, opts) 96 + this.#connection.addEventListener('wsdata', this.#onWSData, opts) 95 97 this.#connection.addEventListener('peerdata', this.#onPeerData, opts) 96 - this.#connection.addEventListener('wsdata', this.#onWSData, opts) 98 + 99 + this.#connection.addEventListener( 100 + 'peeropen', 101 + (e) => void this.dispatchCustomEvent('peeropen', e.detail), 102 + opts, 103 + ) 104 + this.#connection.addEventListener( 105 + 'peerclose', 106 + (e) => void this.dispatchCustomEvent('peerclose', e.detail), 107 + opts, 108 + ) 97 109 } 98 110 99 111 register(upstream: string): void { ··· 101 113 this.connect({upstream, realmid, register: true}) 102 114 } 103 115 116 + async generateInvite() { 117 + if (!this.#connection?.address) return 118 + 119 + const jwt = generateSignableJwt({ 120 + aud: this.#connection.address.realmid, 121 + iss: this.identid, 122 + sub: 'invitation', 123 + jti: nanoid(), 124 + exp: Date.now() / 1000 + 5 * 60, 125 + }) 126 + 127 + return jwt.sign(this.keypair.privateKey) 128 + } 129 + 104 130 exchangeInvite(upstream: string, invitation: string) { 105 131 const token = jwtSchema.parse(invitation) 106 132 const realmid = RealmBrand.parse(token.claims.aud) ··· 119 145 return this.#connection != null && this.#connection.connected 120 146 } 121 147 148 + get peers(): ReadonlyArray<IdentID> { 149 + return this.#connection?.peers || [] 150 + } 151 + 122 152 #onWSOpen = (e: CustomEvent<RealmAddress>) => { 123 153 this.dispatchCustomEvent('connected', e.detail) 124 154 } ··· 142 172 console.error('error on identity action recording', exc, parsed.data) 143 173 }) 144 174 } else { 145 - console.warn('unknown message in identity:', data, parsed.error) 175 + this.dispatchCustomEvent('data', data) 146 176 } 147 177 } 148 178
+189
src/realm/client/realm-peer.ts
··· 1 + import {z} from 'zod/v4' 2 + 3 + import {controllerWithSignals} from '#lib/async/aborts' 4 + import {BlockingQueue} from '#lib/async/blocking-queue' 5 + import {Fence} from '#lib/async/fence' 6 + import {sleep} from '#lib/async/sleep' 7 + import {DataChannelEventMap, DataChannelPeer, DataChannelSendable} from '#lib/client/webrtc' 8 + import {TypedEventTarget} from '#lib/events' 9 + import {jsonCodec} from '#lib/schema' 10 + 11 + import {RealmPingEvent, realmPingEventSchema} from '#realm/protocol/messages-realm' 12 + import {RealmAction} from '#realm/schema' 13 + import {IdentID} from '#realm/schema/brands' 14 + 15 + import {ActionStore} from './action-store' 16 + 17 + const realmPeerMessageSchema = z.union([realmPingEventSchema]) 18 + 19 + type RealmPeerEventMap = DataChannelEventMap & { 20 + peeropen: CustomEvent<{identid: IdentID}> 21 + peerclose: CustomEvent<{identid: IdentID}> 22 + peererror: CustomEvent<{identid: IdentID; error: Error}> 23 + peerdata: CustomEvent<{identid: IdentID; data: string | ArrayBuffer}> 24 + } 25 + 26 + /** 27 + * RealmPeer manages a WebRTC data channel connection to another peer in the realm. 28 + * It handles ping/pong synchronization and action forwarding. 29 + */ 30 + export class RealmPeer extends TypedEventTarget<RealmPeerEventMap> { 31 + #connected: Fence 32 + 33 + #queue: BlockingQueue<string> 34 + #abort: AbortController 35 + 36 + #pingLoopRunning = false 37 + #receiveLoopRunning = false 38 + 39 + constructor( 40 + readonly actions: ActionStore, 41 + readonly identid: IdentID, 42 + readonly initiator: boolean, 43 + readonly channel: DataChannelPeer, 44 + signal?: AbortSignal, 45 + ) { 46 + super() 47 + 48 + this.#abort = controllerWithSignals(signal) 49 + this.#connected = new Fence() 50 + this.#queue = new BlockingQueue() 51 + 52 + const opts = {signal: this.#abort.signal} 53 + this.channel.addEventListener('connect', this.#peerConnected, opts) 54 + this.channel.addEventListener('close', this.#peerClosed, opts) 55 + this.channel.addEventListener('data', this.#peerData, opts) 56 + 57 + this.#pingLoop().catch((exc: unknown) => { 58 + if (this.#abort.signal.aborted) return 59 + console.warn('unexpected error in peer ping loop', exc) 60 + }) 61 + 62 + this.#receiveLoop().catch((exc: unknown) => { 63 + if (this.#abort.signal.aborted) return 64 + console.warn('unexpected error in peer receive loop', exc) 65 + }) 66 + } 67 + 68 + get isConnected(): boolean { 69 + return this.#connected.value 70 + } 71 + 72 + get isDestroyed(): boolean { 73 + return this.#abort.signal.aborted 74 + } 75 + 76 + get pingLoopActive(): boolean { 77 + return this.#pingLoopRunning 78 + } 79 + 80 + get receiveLoopActive(): boolean { 81 + return this.#receiveLoopRunning 82 + } 83 + 84 + connectionStats() { 85 + return this.channel.connectionStats() 86 + } 87 + 88 + send(data: DataChannelSendable) { 89 + this.channel.send(data) 90 + } 91 + 92 + signal(data: unknown) { 93 + this.channel.signal(data) 94 + } 95 + 96 + destroy(error?: Error) { 97 + this.#abort.abort(error || 'shutting down') 98 + this.#connected.close() 99 + this.channel.destroy() 100 + } 101 + 102 + /// 103 + 104 + #peerConnected = () => { 105 + this.#connected.open() 106 + } 107 + 108 + #peerClosed = () => { 109 + this.#connected.close() 110 + } 111 + 112 + #peerData = (event: CustomEvent<string | ArrayBuffer>) => { 113 + const data = event.detail 114 + const string = typeof data === 'string' ? data : new TextDecoder().decode(data) 115 + this.#queue.enqueue(string) 116 + } 117 + 118 + /// 119 + 120 + #receiveLoop = async () => { 121 + this.#receiveLoopRunning = true 122 + try { 123 + this.#abort.signal.throwIfAborted() 124 + 125 + while (await this.#connected.enter(this.#abort.signal)) { 126 + const payload = await this.#queue.dequeue(this.#abort.signal) 127 + if (!this.#connected.value) break 128 + 129 + const parsed = await jsonCodec(realmPeerMessageSchema).safeParseAsync(payload) 130 + if (parsed.success) { 131 + await this.#receivePing(parsed.data) 132 + } else { 133 + this.dispatchCustomEvent('peerdata', {identid: this.identid, data: payload}) 134 + } 135 + } 136 + } finally { 137 + this.#receiveLoopRunning = false 138 + } 139 + } 140 + 141 + #pingLoop = async () => { 142 + this.#pingLoopRunning = true 143 + try { 144 + while (await this.#connected.enter(this.#abort.signal)) { 145 + // wait a moment to let the connect settle before ping 146 + await sleep(100, this.#abort.signal) 147 + if (!this.#connected.value) { 148 + break 149 + } 150 + 151 + await this.#sendPing() 152 + await sleep(30_000, this.#abort.signal) 153 + } 154 + } finally { 155 + this.#pingLoopRunning = false 156 + } 157 + } 158 + 159 + #sendPing = async () => { 160 + if (!this.#connected.value) return 161 + 162 + const clocks = await this.actions.buildSyncState() 163 + 164 + const message: RealmPingEvent = { 165 + typ: 'evt', 166 + msg: 'realm.ping', 167 + dat: {clocks, requestSync: true}, 168 + } 169 + 170 + this.channel.send(JSON.stringify(message)) 171 + } 172 + 173 + #receivePing = async (ping: RealmPingEvent) => { 174 + if (!ping.dat.requestSync) return 175 + 176 + // TODO: record last received ping 177 + 178 + this.#sendActions(await this.actions.buildSyncDelta(ping.dat.clocks)) 179 + } 180 + 181 + /// 182 + 183 + #sendActions(actions: RealmAction[], batch_size = 100) { 184 + for (let i = 0; i < actions.length; i += batch_size) { 185 + const batch = actions.slice(i, i + batch_size) 186 + this.channel.send(JSON.stringify(batch)) 187 + } 188 + } 189 + }
+10 -8
src/realm/logical-clock.spec.ts
··· 1 - import {beforeEach, describe, expect, it, jest} from '@jest/globals' 1 + import {beforeEach, describe, expect, it, vi} from 'vitest' 2 2 3 3 import {IdentBrand} from '#realm/schema/brands' 4 - import {compare, explode, generate} from '#realm/schema/timestamp' 4 + import {Timestamp, compare, explode, generate} from '#realm/schema/timestamp' 5 5 6 6 import {LogicalClock} from './logical-clock' 7 + 8 + type TickListener = (evt: CustomEvent<Timestamp>) => void 7 9 8 10 describe('LogicalClock', () => { 9 11 let identAlice: ReturnType<typeof IdentBrand.generate> ··· 208 210 describe('tick event', () => { 209 211 it('dispatches tick event when clock advances', () => { 210 212 const clock = new LogicalClock(identAlice) 211 - const listener = jest.fn() 213 + const listener = vi.fn<TickListener>() 212 214 213 215 clock.addEventListener('tick', listener) 214 216 ··· 226 228 227 229 it('does not dispatch event when clock does not advance', () => { 228 230 const clock = new LogicalClock(identAlice) 229 - const listener = jest.fn() 231 + const listener = vi.fn<TickListener>() 230 232 231 233 const ts1 = generate(identAlice, 2000, 0) 232 234 const ts2 = generate(identAlice, 1000, 0) ··· 240 242 241 243 it('dispatches event on each now() call', () => { 242 244 const clock = new LogicalClock(identAlice) 243 - const listener = jest.fn() 245 + const listener = vi.fn<TickListener>() 244 246 245 247 clock.addEventListener('tick', listener) 246 248 ··· 253 255 254 256 it('allows multiple listeners', () => { 255 257 const clock = new LogicalClock(identAlice) 256 - const listener1 = jest.fn() 257 - const listener2 = jest.fn() 258 + const listener1 = vi.fn<TickListener>() 259 + const listener2 = vi.fn<TickListener>() 258 260 259 261 clock.addEventListener('tick', listener1) 260 262 clock.addEventListener('tick', listener2) ··· 268 270 269 271 it('can remove listeners', () => { 270 272 const clock = new LogicalClock(identAlice) 271 - const listener = jest.fn() 273 + const listener = vi.fn<TickListener>() 272 274 273 275 clock.addEventListener('tick', listener) 274 276 clock.tick(generate(identAlice, 1000, 0))
+649
src/realm/protocol/peer-protocol.spec.ts
··· 1 + import {beforeEach, describe, expect, it} from 'vitest' 2 + import {z} from 'zod/v4' 3 + 4 + import {MockDataChannel, createChannelPair} from '#spec/helpers-socket-pair' 5 + 6 + import {sleep} from '#lib/async/sleep' 7 + import {DataChannelPeer} from '#lib/client/webrtc' 8 + import {jsonCodec} from '#lib/schema' 9 + 10 + import {ActionStore} from '#realm/client/action-store' 11 + import {RealmPeer} from '#realm/client/realm-peer' 12 + import {RealmPingEvent, realmPingEventSchema} from '#realm/protocol/messages-realm' 13 + import {RealmAction, actionSchema} from '#realm/schema' 14 + import {IdentBrand, IdentID} from '#realm/schema/brands' 15 + import {generate as generateTimestamp} from '#realm/schema/timestamp' 16 + 17 + import {MockActionStore} from './spec/mock-action-store' 18 + 19 + describe('peer protocol', () => { 20 + let identid1: IdentID 21 + let identid2: IdentID 22 + let actions1: MockActionStore 23 + let actions2: MockActionStore 24 + 25 + beforeEach(() => { 26 + identid1 = IdentBrand.generate() 27 + identid2 = IdentBrand.generate() 28 + actions1 = new MockActionStore() 29 + actions2 = new MockActionStore() 30 + }) 31 + 32 + function createPeerPair(autoConnect = true): [RealmPeer, RealmPeer, MockDataChannel, MockDataChannel] { 33 + const [chan1, chan2] = createChannelPair(autoConnect) 34 + 35 + const peer1 = new RealmPeer( 36 + actions1 as unknown as ActionStore, 37 + identid2, 38 + true, 39 + chan1 as unknown as DataChannelPeer, 40 + ) 41 + 42 + const peer2 = new RealmPeer( 43 + actions2 as unknown as ActionStore, 44 + identid1, 45 + false, 46 + chan2 as unknown as DataChannelPeer, 47 + ) 48 + 49 + return [peer1, peer2, chan1, chan2] 50 + } 51 + 52 + describe('error handling and recovery', () => { 53 + it('continues operating after channel error events', async () => { 54 + const [chan1, chan2] = createChannelPair(true) 55 + 56 + const peer1 = new RealmPeer( 57 + actions1 as unknown as ActionStore, 58 + identid2, 59 + true, 60 + chan1 as unknown as DataChannelPeer, 61 + ) 62 + 63 + const errors: unknown[] = [] 64 + chan1.addEventListener('error', (e) => { 65 + errors.push(e) 66 + }) 67 + 68 + // Verify initially working 69 + await sleep(10) 70 + expect(peer1.isConnected).toBe(true) 71 + expect(peer1.pingLoopActive).toBe(true) 72 + 73 + // Simulate an error event (but not a disconnect) 74 + chan1.dispatchCustomEvent('error', new Error('Network hiccup')) 75 + await sleep(10) 76 + 77 + // Should still be connected and active 78 + expect(peer1.isConnected).toBe(true) 79 + expect(peer1.pingLoopActive).toBe(true) 80 + expect(peer1.receiveLoopActive).toBe(true) 81 + expect(errors).toHaveLength(1) 82 + 83 + // Should still be able to send data 84 + peer1.send('test message') 85 + const message = await chan2.nextMessage() 86 + expect(message).toBe('test message') 87 + 88 + peer1.destroy() 89 + }) 90 + 91 + it('handles rapid connect/disconnect cycles without leaking', async () => { 92 + const [chan1, chan2] = createChannelPair() 93 + 94 + const peer1 = new RealmPeer( 95 + actions1 as unknown as ActionStore, 96 + identid2, 97 + true, 98 + chan1 as unknown as DataChannelPeer, 99 + ) 100 + 101 + // Rapid cycling 102 + for (let i = 0; i < 5; i++) { 103 + chan1.connect() 104 + await sleep(5) 105 + expect(peer1.isConnected).toBe(true) 106 + 107 + chan1.disconnect() 108 + await sleep(5) 109 + expect(peer1.isConnected).toBe(false) 110 + } 111 + 112 + // Final connect 113 + chan1.connect() 114 + await sleep(150) 115 + 116 + // Should still function normally 117 + const message = await chan2.nextMessage() 118 + const ping = jsonCodec(realmPingEventSchema).parse(message) 119 + expect(ping.msg).toBe('realm.ping') 120 + 121 + // Loops should still be running (only one instance each) 122 + expect(peer1.pingLoopActive).toBe(true) 123 + expect(peer1.receiveLoopActive).toBe(true) 124 + 125 + peer1.destroy() 126 + }) 127 + }) 128 + 129 + describe('connection lifecycle', () => { 130 + it('emits connect event when channel connects', async () => { 131 + const [chan1, _] = createChannelPair() 132 + 133 + let connected = false 134 + const peer1 = new RealmPeer( 135 + actions1 as unknown as ActionStore, 136 + identid2, 137 + true, 138 + chan1 as unknown as DataChannelPeer, 139 + ) 140 + 141 + peer1.channel.addEventListener('connect', () => { 142 + connected = true 143 + }) 144 + 145 + chan1.connect() 146 + 147 + await sleep(10) 148 + expect(connected).toBe(true) 149 + 150 + peer1.destroy() 151 + }) 152 + 153 + it('emits close event when destroyed', async () => { 154 + const [peer1, peer2] = createPeerPair() 155 + 156 + let closed = false 157 + peer1.channel.addEventListener('close', () => { 158 + closed = true 159 + }) 160 + 161 + peer1.destroy() 162 + 163 + await sleep(10) 164 + expect(closed).toBe(true) 165 + 166 + peer2.destroy() 167 + }) 168 + 169 + it('tracks loop status', async () => { 170 + const [peer1, peer2] = createPeerPair() 171 + 172 + // Loops should be running after connection 173 + await sleep(10) 174 + expect(peer1.isConnected).toBe(true) 175 + expect(peer1.isDestroyed).toBe(false) 176 + expect(peer1.pingLoopActive).toBe(true) 177 + expect(peer1.receiveLoopActive).toBe(true) 178 + 179 + // Destroy peer 180 + peer1.destroy() 181 + await sleep(10) 182 + 183 + expect(peer1.isConnected).toBe(false) 184 + expect(peer1.isDestroyed).toBe(true) 185 + expect(peer1.pingLoopActive).toBe(false) 186 + expect(peer1.receiveLoopActive).toBe(false) 187 + 188 + peer2.destroy() 189 + }) 190 + 191 + it('stops loops when destroyed', async () => { 192 + const [peer1, peer2, _, chan2] = createPeerPair() 193 + 194 + // Verify loops are running 195 + await sleep(10) 196 + expect(peer1.pingLoopActive).toBe(true) 197 + expect(peer1.receiveLoopActive).toBe(true) 198 + 199 + // Get initial ping 200 + await sleep(150) 201 + await chan2.nextMessage() // consume first ping 202 + expect(chan2.empty).toBe(true) 203 + 204 + // Destroy the peer 205 + peer1.destroy() 206 + 207 + // Loops should stop quickly 208 + await sleep(50) 209 + expect(peer1.pingLoopActive).toBe(false) 210 + expect(peer1.receiveLoopActive).toBe(false) 211 + 212 + // Should not receive any more pings even after waiting 213 + await sleep(200) 214 + expect(chan2.empty).toBe(true) 215 + 216 + // Verify peer can't send anymore 217 + expect(() => { 218 + peer1.send('should fail') 219 + }).not.toThrow() 220 + await sleep(10) 221 + expect(chan2.empty).toBe(true) // Nothing received 222 + 223 + // Verify destroyed state persists 224 + expect(peer1.isDestroyed).toBe(true) 225 + expect(peer1.isConnected).toBe(false) 226 + 227 + peer2.destroy() 228 + }) 229 + 230 + it('peer loops continue when channel temporarily disconnects', async () => { 231 + const [chan1, chan2] = createChannelPair() 232 + 233 + const peer1 = new RealmPeer( 234 + actions1 as unknown as ActionStore, 235 + identid2, 236 + true, 237 + chan1 as unknown as DataChannelPeer, 238 + ) 239 + 240 + // Connect initially 241 + chan1.connect() 242 + await sleep(10) 243 + 244 + expect(peer1.isConnected).toBe(true) 245 + expect(peer1.pingLoopActive).toBe(true) 246 + expect(peer1.receiveLoopActive).toBe(true) 247 + 248 + // Simulate temporary disconnect (not destroy) 249 + chan1.disconnect() 250 + await sleep(10) 251 + 252 + expect(peer1.isConnected).toBe(false) 253 + // Loops should still be active, waiting to reconnect 254 + expect(peer1.pingLoopActive).toBe(true) 255 + expect(peer1.receiveLoopActive).toBe(true) 256 + expect(peer1.isDestroyed).toBe(false) 257 + 258 + // Reconnect 259 + chan1.connect() 260 + await sleep(10) 261 + 262 + expect(peer1.isConnected).toBe(true) 263 + expect(peer1.pingLoopActive).toBe(true) 264 + expect(peer1.receiveLoopActive).toBe(true) 265 + 266 + // Should resume sending pings after reconnect 267 + await sleep(150) 268 + const message = await chan2.nextMessage() 269 + const ping = jsonCodec(realmPingEventSchema).parse(message) 270 + expect(ping.msg).toBe('realm.ping') 271 + 272 + peer1.destroy() 273 + }) 274 + 275 + it('handles multiple connect/disconnect cycles', async () => { 276 + const [chan1, chan2] = createChannelPair() 277 + 278 + const peer1 = new RealmPeer( 279 + actions1 as unknown as ActionStore, 280 + identid2, 281 + true, 282 + chan1 as unknown as DataChannelPeer, 283 + ) 284 + 285 + // Cycle 1: connect 286 + chan1.connect() 287 + await sleep(10) 288 + expect(peer1.isConnected).toBe(true) 289 + 290 + // Cycle 1: disconnect 291 + chan1.disconnect() 292 + await sleep(10) 293 + expect(peer1.isConnected).toBe(false) 294 + expect(peer1.pingLoopActive).toBe(true) // Still active 295 + 296 + // Cycle 2: connect 297 + chan1.connect() 298 + await sleep(10) 299 + expect(peer1.isConnected).toBe(true) 300 + 301 + // Cycle 2: disconnect 302 + chan1.disconnect() 303 + await sleep(10) 304 + expect(peer1.isConnected).toBe(false) 305 + 306 + // Cycle 3: connect 307 + chan1.connect() 308 + await sleep(150) 309 + 310 + // Should still be functional 311 + const message = await chan2.nextMessage() 312 + const ping = jsonCodec(realmPingEventSchema).parse(message) 313 + expect(ping.msg).toBe('realm.ping') 314 + 315 + peer1.destroy() 316 + }) 317 + 318 + it('receive loop continues processing after reconnect', async () => { 319 + const [chan1, chan2] = createChannelPair() 320 + 321 + const peer1 = new RealmPeer( 322 + actions1 as unknown as ActionStore, 323 + identid2, 324 + true, 325 + chan1 as unknown as DataChannelPeer, 326 + ) 327 + 328 + const receivedData: unknown[] = [] 329 + peer1.addEventListener('peerdata', (event) => { 330 + receivedData.push(event.detail) 331 + }) 332 + 333 + // Connect and send first message 334 + chan1.connect() 335 + await sleep(10) 336 + chan2.send(JSON.stringify({msg: 'first'})) 337 + await sleep(10) 338 + expect(receivedData).toHaveLength(1) 339 + 340 + // Disconnect 341 + chan1.disconnect() 342 + await sleep(10) 343 + 344 + // Messages sent while disconnected are dropped (WebRTC behavior) 345 + chan2.send(JSON.stringify({msg: 'while_disconnected'})) 346 + await sleep(10) 347 + // Should still be 1 - message was dropped 348 + expect(receivedData).toHaveLength(1) 349 + 350 + // Reconnect 351 + chan1.connect() 352 + await sleep(50) 353 + 354 + // Still just the first message 355 + expect(receivedData).toHaveLength(1) 356 + 357 + // Send another after reconnect - this should work 358 + chan2.send(JSON.stringify({msg: 'after_reconnect'})) 359 + await sleep(10) 360 + expect(receivedData).toHaveLength(2) 361 + expect(receivedData[1]).toMatchObject({ 362 + data: JSON.stringify({msg: 'after_reconnect'}), 363 + }) 364 + 365 + peer1.destroy() 366 + }) 367 + }) 368 + 369 + describe('ping protocol', () => { 370 + it('sends initial ping quickly after connection', async () => { 371 + const [peer1, peer2, _, chan2] = createPeerPair() 372 + 373 + // Should get first ping within ~100ms (plus processing time) 374 + const start = Date.now() 375 + const message = await chan2.nextMessage() 376 + const elapsed = Date.now() - start 377 + 378 + expect(elapsed).toBeLessThan(200) // Should be ~100ms + overhead 379 + 380 + const ping = jsonCodec(realmPingEventSchema).parse(message) 381 + expect(ping.msg).toBe('realm.ping') 382 + 383 + peer1.destroy() 384 + peer2.destroy() 385 + }) 386 + 387 + it('sends ping after connecting', async () => { 388 + const [peer1, peer2, _, chan2] = createPeerPair() 389 + 390 + // wait for ping loop get going 391 + await sleep(150) 392 + 393 + const message = await chan2.nextMessage() 394 + const ping = jsonCodec(realmPingEventSchema).parse(message) 395 + 396 + expect(ping).toMatchObject({ 397 + typ: 'evt', 398 + msg: 'realm.ping', 399 + dat: { 400 + clocks: {}, 401 + requestSync: true, 402 + }, 403 + }) 404 + 405 + peer1.destroy() 406 + peer2.destroy() 407 + }) 408 + 409 + it('includes clocks in ping', async () => { 410 + const timestamp = generateTimestamp(identid1, Math.floor(Date.now() / 1000), 0) 411 + actions1.setClock(identid1, timestamp) 412 + 413 + const [peer1, peer2, _, chan2] = createPeerPair() 414 + 415 + // wait for ping loop to get going 416 + await sleep(150) 417 + 418 + const message = await chan2.nextMessage() 419 + const ping = jsonCodec(realmPingEventSchema).parse(message) 420 + 421 + expect(ping.dat.clocks[identid1]).toBe(timestamp) 422 + 423 + peer1.destroy() 424 + peer2.destroy() 425 + }) 426 + 427 + // this will change in the future when we do selective syncing, 428 + // but it will be driven by the connection, not the peer 429 + it('always sets requestSync to true', async () => { 430 + const [peer1, peer2, _, chan2] = createPeerPair() 431 + await sleep(150) 432 + 433 + const message = await chan2.nextMessage() 434 + const ping = jsonCodec(realmPingEventSchema).parse(message) 435 + 436 + expect(ping.dat.requestSync).toBe(true) 437 + 438 + peer1.destroy() 439 + peer2.destroy() 440 + }) 441 + }) 442 + 443 + describe('sync protocol', () => { 444 + it('sends actions when receiving ping with requestSync', async () => { 445 + const timestamp = generateTimestamp(identid1, Math.floor(Date.now() / 1000), 0) 446 + const [peer1, peer2, _, chan2] = createPeerPair() 447 + 448 + const action: RealmAction = { 449 + typ: 'act', 450 + msg: 'test.action', 451 + clk: timestamp, 452 + dat: {value: 42}, 453 + } 454 + actions1.addAction(action) 455 + 456 + // peer 2 sends ping requesting sync 457 + const pingEvent: RealmPingEvent = { 458 + typ: 'evt', 459 + msg: 'realm.ping', 460 + dat: { 461 + clocks: {}, 462 + requestSync: true, 463 + }, 464 + } 465 + chan2.send(JSON.stringify(pingEvent)) 466 + 467 + // peer 1 should respond with actions 468 + await sleep(50) 469 + const message = await chan2.nextMessage() 470 + const actions = jsonCodec(z.array(actionSchema)).parse(message) 471 + 472 + expect(actions).toHaveLength(1) 473 + expect(actions[0]).toMatchObject({ 474 + typ: 'act', 475 + msg: 'test.action', 476 + clk: timestamp, 477 + dat: {value: 42}, 478 + }) 479 + 480 + peer1.destroy() 481 + peer2.destroy() 482 + }) 483 + 484 + it('does not send actions when requestSync is false', async () => { 485 + const timestamp = generateTimestamp(identid1, Math.floor(Date.now() / 1000), 0) 486 + actions1.addAction({ 487 + typ: 'act', 488 + msg: 'test.action', 489 + clk: timestamp, 490 + dat: {value: 42}, 491 + }) 492 + 493 + const [peer1, peer2, _, chan2] = createPeerPair() 494 + 495 + // peer 2 sends ping without requesting sync 496 + const pingEvent: RealmPingEvent = { 497 + typ: 'evt', 498 + msg: 'realm.ping', 499 + dat: { 500 + clocks: {}, 501 + requestSync: false, 502 + }, 503 + } 504 + chan2.send(JSON.stringify(pingEvent)) 505 + 506 + // wait a bit - no actions should be sent 507 + await sleep(50) 508 + 509 + // drain peer1's auto-ping first 510 + const autoMessage = await chan2.nextMessage() 511 + const autoPing = jsonCodec(realmPingEventSchema).safeParse(autoMessage) 512 + expect(autoPing.success).toBe(true) 513 + 514 + // no further messages should be queued (actions were not sent) 515 + // we can't easily test absence, so just verify the auto-ping was all we got 516 + expect(chan2.empty).toBe(true) 517 + 518 + peer1.destroy() 519 + peer2.destroy() 520 + }) 521 + 522 + it('sends only actions the remote does not have', async () => { 523 + const ts1 = generateTimestamp(identid1, Math.floor(Date.now() / 1000), 0) 524 + const ts2 = generateTimestamp(identid1, Math.floor(Date.now() / 1000), 1) 525 + 526 + actions1.addAction({typ: 'act', msg: 'old.action', clk: ts1, dat: {value: 1}}) 527 + actions1.addAction({typ: 'act', msg: 'new.action', clk: ts2, dat: {value: 2}}) 528 + 529 + const [peer1, peer2, _, chan2] = createPeerPair() 530 + 531 + // peer 2 already has ts1 532 + const pingEvent: RealmPingEvent = { 533 + typ: 'evt', 534 + msg: 'realm.ping', 535 + dat: { 536 + clocks: {[identid1]: ts1}, 537 + requestSync: true, 538 + }, 539 + } 540 + chan2.send(JSON.stringify(pingEvent)) 541 + 542 + await sleep(50) 543 + const message = await chan2.nextMessage() 544 + const actions = jsonCodec(z.array(actionSchema)).parse(message) 545 + 546 + // should only receive the newer action 547 + expect(actions).toHaveLength(1) 548 + expect(actions[0].clk).toBe(ts2) 549 + 550 + peer1.destroy() 551 + peer2.destroy() 552 + }) 553 + 554 + it('batches large action sets', async () => { 555 + // Add 150 actions (more than batch size of 100) 556 + for (let i = 0; i < 150; i++) { 557 + const ts = generateTimestamp(identid1, Math.floor(Date.now() / 1000), i) 558 + actions1.addAction({typ: 'act', msg: 'batch.action', clk: ts, dat: {index: i}}) 559 + } 560 + 561 + const [peer1, peer2, _, chan2] = createPeerPair() 562 + const pingEvent: RealmPingEvent = { 563 + typ: 'evt', 564 + msg: 'realm.ping', 565 + dat: { 566 + clocks: {}, 567 + requestSync: true, 568 + }, 569 + } 570 + chan2.send(JSON.stringify(pingEvent)) 571 + await sleep(50) 572 + 573 + // should receive two batches 574 + const batch1 = await chan2.nextMessage() 575 + const actions1Parsed = jsonCodec(z.array(actionSchema)).parse(batch1) 576 + expect(actions1Parsed).toHaveLength(100) 577 + 578 + const batch2 = await chan2.nextMessage() 579 + const actions2Parsed = jsonCodec(z.array(actionSchema)).parse(batch2) 580 + expect(actions2Parsed).toHaveLength(50) 581 + 582 + expect(chan2.empty).toBe(true) 583 + 584 + peer1.destroy() 585 + peer2.destroy() 586 + }) 587 + }) 588 + 589 + describe('data forwarding', () => { 590 + it('emits peerdata event for non-ping messages', async () => { 591 + const [peer1, peer2, _, chan2] = createPeerPair() 592 + 593 + const receivedData: unknown[] = [] 594 + peer1.addEventListener('peerdata', (event) => { 595 + receivedData.push(event.detail) 596 + }) 597 + 598 + // send arbitrary data 599 + chan2.send(JSON.stringify({custom: 'message'})) 600 + await sleep(50) 601 + 602 + expect(receivedData).toHaveLength(1) 603 + expect(receivedData[0]).toMatchObject({ 604 + identid: identid2, // peer1's remote is identid2 605 + data: JSON.stringify({custom: 'message'}), 606 + }) 607 + 608 + peer1.destroy() 609 + peer2.destroy() 610 + }) 611 + 612 + it('does not emit peerdata for ping messages', async () => { 613 + const [peer1, peer2, _, chan2] = createPeerPair() 614 + 615 + const receivedData: unknown[] = [] 616 + peer1.addEventListener('peerdata', (event) => { 617 + receivedData.push(event.detail) 618 + }) 619 + 620 + const pingEvent: RealmPingEvent = { 621 + typ: 'evt', 622 + msg: 'realm.ping', 623 + dat: {clocks: {}, requestSync: false}, 624 + } 625 + chan2.send(JSON.stringify(pingEvent)) 626 + await sleep(50) 627 + 628 + // pings should be handled internally, not forwarded 629 + expect(receivedData).toHaveLength(0) 630 + 631 + peer1.destroy() 632 + peer2.destroy() 633 + }) 634 + }) 635 + 636 + describe('send method', () => { 637 + it('sends data through channel', async () => { 638 + const [peer1, peer2, _, chan2] = createPeerPair() 639 + 640 + peer1.send('hello from peer1') 641 + 642 + const message = await chan2.nextMessage() 643 + expect(message).toBe('hello from peer1') 644 + 645 + peer1.destroy() 646 + peer2.destroy() 647 + }) 648 + }) 649 + })
+511
src/realm/protocol/realm-protocol.spec.ts
··· 1 + import {afterEach, beforeEach, describe, expect, it} from 'vitest' 2 + import {z} from 'zod/v4' 3 + 4 + import {MockSocket, createSocketPair} from '#spec/helpers-socket-pair' 5 + 6 + import {sleep} from '#lib/async/sleep' 7 + import {generateSignableJwt, generateSigningJwkPair, jwkExport} from '#lib/crypto/jwks' 8 + import {jwtPayload} from '#lib/crypto/jwts' 9 + import {exceptionMessageSchema, jsonCodec} from '#lib/schema' 10 + 11 + import { 12 + RealmBroadcastEvent, 13 + RealmPeerLeftEvent, 14 + RealmPingEvent, 15 + RealmSignalEvent, 16 + realmPeerLeftEventSchema, 17 + realmPingEventSchema, 18 + realmSignalEventSchema, 19 + } from '#realm/protocol/messages-realm' 20 + import {RealmAction, actionSchema} from '#realm/schema' 21 + import {IdentBrand, RealmBrand, RealmID} from '#realm/schema/brands' 22 + import {generate as generateTimestamp} from '#realm/schema/timestamp' 23 + import {AuthenticatedIdentity} from '#realm/server/driver-preauth' 24 + import {driveRealm} from '#realm/server/driver-realm' 25 + import {Realm} from '#realm/server/realm' 26 + 27 + describe('realm protocol', () => { 28 + let realm: Realm 29 + let realmid: RealmID 30 + 31 + beforeEach(() => { 32 + realmid = RealmBrand.generate() 33 + realm = new Realm(realmid, ':memory:') 34 + }) 35 + 36 + afterEach(() => { 37 + realm.shutdown() 38 + }) 39 + 40 + async function createPeer(signal?: AbortSignal): Promise<{ 41 + client: MockSocket 42 + auth: AuthenticatedIdentity 43 + promise: Promise<void> 44 + }> { 45 + const identid = IdentBrand.generate() 46 + const keyPair = await generateSigningJwkPair() 47 + const pubkey = keyPair.publicKey 48 + const pubjwk = await jwkExport.parseAsync(pubkey) 49 + await realm.admitIdentity(identid, pubkey) 50 + 51 + const [client, server] = createSocketPair() 52 + await sleep(0) // pair needs to start 53 + 54 + realm.attachSocket(identid, server) 55 + 56 + const auth = {realm, identid, pubkey, pubjwk} 57 + const promise = driveRealm(server, auth, signal) 58 + 59 + return {client, auth, promise} 60 + } 61 + 62 + describe('connection lifecycle', () => { 63 + it('broadcasts peer-joined when a peer connects', async () => { 64 + const {client: client1, promise: promise1} = await createPeer() 65 + const {client: client2, auth: auth2, promise: promise2} = await createPeer() 66 + 67 + // Client 1 should receive peer-joined for client 2 68 + const message = await client1.nextMessage() 69 + const event = JSON.parse(message) 70 + 71 + expect(event).toMatchObject({ 72 + typ: 'evt', 73 + msg: 'realm.peer-joined', 74 + dat: { 75 + identid: auth2.identid, 76 + }, 77 + }) 78 + 79 + client1.close() 80 + client2.close() 81 + await promise1 82 + await promise2 83 + }, 2000) 84 + 85 + it('broadcasts peer-left when a peer disconnects', async () => { 86 + const {client: client1, auth: auth1, promise: promise1} = await createPeer() 87 + const {client: client2, promise: promise2} = await createPeer() 88 + 89 + // Client 1 receives peer-joined for client 2 90 + await client1.nextMessage() 91 + 92 + // Close client 1 93 + client1.close() 94 + await promise1 95 + 96 + // Client 2 should receive peer-left event 97 + const message = await client2.nextMessage() 98 + const event = jsonCodec(realmPeerLeftEventSchema).parse(message) 99 + 100 + expect(event).toMatchObject({ 101 + typ: 'evt', 102 + msg: 'realm.peer-left', 103 + dat: { 104 + identid: auth1.identid, 105 + }, 106 + } satisfies RealmPeerLeftEvent) 107 + 108 + client2.close() 109 + await promise2 110 + }) 111 + }, 2000) 112 + 113 + describe('realm.ping', () => { 114 + it('responds with current clocks', async () => { 115 + const {client, auth, promise} = await createPeer() 116 + 117 + // Record an action to populate clocks 118 + const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 119 + realm.recordActions([ 120 + { 121 + typ: 'act', 122 + msg: 'test.action', 123 + clk: timestamp, 124 + dat: {value: 1}, 125 + } satisfies RealmAction, 126 + ]) 127 + 128 + // Send ping 129 + const pingEvent: RealmPingEvent = { 130 + typ: 'evt', 131 + msg: 'realm.ping', 132 + dat: { 133 + clocks: {}, 134 + requestSync: false, 135 + }, 136 + } 137 + client.send(JSON.stringify(pingEvent)) 138 + 139 + // Receive response 140 + const message = await client.nextMessage() 141 + const response = jsonCodec(realmPingEventSchema).parse(message) 142 + 143 + expect(response).toMatchObject({ 144 + typ: 'evt', 145 + msg: 'realm.ping', 146 + dat: { 147 + clocks: { 148 + [auth.identid]: timestamp, 149 + }, 150 + }, 151 + } satisfies RealmPingEvent) 152 + 153 + client.close() 154 + await promise 155 + }, 2000) 156 + 157 + it('sends sync delta when requestSync is true', async () => { 158 + const {client, auth, promise} = await createPeer() 159 + 160 + const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 161 + realm.recordActions([ 162 + { 163 + typ: 'act', 164 + msg: 'test.action', 165 + clk: timestamp, 166 + dat: {value: 1}, 167 + } satisfies RealmAction, 168 + ]) 169 + 170 + // Send ping with sync request 171 + const pingEvent: RealmPingEvent = { 172 + typ: 'evt', 173 + msg: 'realm.ping', 174 + dat: { 175 + clocks: {}, 176 + requestSync: true, 177 + }, 178 + } 179 + client.send(JSON.stringify(pingEvent)) 180 + 181 + // Receive ping response 182 + const pingMessage = await client.nextMessage() 183 + const pingResponse = jsonCodec(realmPingEventSchema).safeParse(pingMessage) 184 + expect(pingResponse.success).toBe(true) 185 + 186 + // Receive sync delta 187 + const syncMessage = await client.nextMessage() 188 + const syncResponse = jsonCodec(z.array(actionSchema)).safeParse(syncMessage) 189 + expect(syncResponse.success).toBe(true) 190 + expect(syncResponse.data).toHaveLength(1) 191 + expect(syncResponse.data).toContainEqual({ 192 + typ: 'act', 193 + msg: 'test.action', 194 + clk: timestamp, 195 + dat: {value: 1}, 196 + }) 197 + 198 + client.close() 199 + await promise 200 + }, 2000) 201 + 202 + it('accepts device caps and info', async () => { 203 + const {client, promise} = await createPeer() 204 + 205 + const pingEvent: RealmPingEvent = { 206 + typ: 'evt', 207 + msg: 'realm.ping', 208 + dat: { 209 + clocks: {}, 210 + requestSync: false, 211 + deviceCaps: { 212 + corsFetch: false, 213 + networkQuality: 3, 214 + }, 215 + deviceInfo: { 216 + name: 'test-client', 217 + ua: 'test-ua', 218 + }, 219 + }, 220 + } 221 + client.send(JSON.stringify(pingEvent)) 222 + 223 + const message = await client.nextMessage() 224 + const response = jsonCodec(realmPingEventSchema).parse(message) 225 + 226 + expect(response).toMatchObject({ 227 + typ: 'evt', 228 + msg: 'realm.ping', 229 + dat: expect.objectContaining({ 230 + clocks: expect.any(Object), 231 + }), 232 + }) 233 + 234 + client.close() 235 + await promise 236 + }, 2000) 237 + }) 238 + 239 + describe('action recording', () => { 240 + it('records actions sent by client', async () => { 241 + const {client, auth, promise} = await createPeer() 242 + 243 + const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 244 + const actions: RealmAction[] = [ 245 + { 246 + typ: 'act', 247 + msg: 'test.action', 248 + clk: timestamp, 249 + dat: {value: 1}, 250 + }, 251 + ] 252 + client.send(JSON.stringify(actions)) 253 + 254 + // Wait for processing 255 + await new Promise((resolve) => setTimeout(resolve, 10)) 256 + 257 + const syncState = realm.buildSyncState() 258 + expect(syncState[auth.identid]).toBe(timestamp) 259 + 260 + client.close() 261 + await promise 262 + }, 2000) 263 + 264 + it('handles multiple actions in a batch', async () => { 265 + const {client, auth, promise} = await createPeer() 266 + 267 + const ts1 = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 268 + const ts2 = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 1) 269 + const actions: RealmAction[] = [ 270 + {typ: 'act', msg: 'test.action1', clk: ts1, dat: {value: 1}}, 271 + {typ: 'act', msg: 'test.action2', clk: ts2, dat: {value: 2}}, 272 + ] 273 + 274 + client.send(JSON.stringify(actions)) 275 + await sleep(150) 276 + 277 + const syncState = realm.buildSyncState() 278 + expect(syncState[auth.identid]).toBe(ts2) 279 + 280 + const delta = realm.buildSyncDelta({}) 281 + expect(delta).toHaveLength(2) 282 + 283 + client.close() 284 + await promise 285 + }, 2000) 286 + 287 + it('syncs actions between peers via ping', async () => { 288 + const {client: client1, auth: auth1, promise: promise1} = await createPeer() 289 + const {client: client2, promise: promise2} = await createPeer() 290 + 291 + // Client 1 receives peer-joined 292 + await client1.nextMessage() 293 + 294 + // Client 1 sends an action 295 + const timestamp = generateTimestamp(auth1.identid, Math.floor(Date.now() / 1000), 0) 296 + const action: RealmAction = { 297 + typ: 'act', 298 + msg: 'test.action', 299 + clk: timestamp, 300 + dat: {value: 42}, 301 + } 302 + client1.send(JSON.stringify([action])) 303 + 304 + // Wait for processing 305 + await new Promise((resolve) => setTimeout(resolve, 10)) 306 + 307 + // Client 2 requests sync 308 + const pingEvent: RealmPingEvent = { 309 + typ: 'evt', 310 + msg: 'realm.ping', 311 + dat: { 312 + clocks: {}, 313 + requestSync: true, 314 + }, 315 + } 316 + client2.send(JSON.stringify(pingEvent)) 317 + 318 + // Client 2 receives ping response 319 + const pingResponse = await client2.nextMessage() 320 + const parsedPing = JSON.parse(pingResponse) 321 + expect(parsedPing.dat.clocks[auth1.identid]).toBe(timestamp) 322 + 323 + // Client 2 receives sync delta 324 + const syncDelta = await client2.nextMessage() 325 + const actions = JSON.parse(syncDelta) 326 + 327 + expect(actions).toHaveLength(1) 328 + expect(actions[0]).toMatchObject({ 329 + typ: 'act', 330 + msg: 'test.action', 331 + clk: timestamp, 332 + dat: {value: 42}, 333 + }) 334 + 335 + client1.close() 336 + client2.close() 337 + await promise1 338 + await promise2 339 + }) 340 + }, 2000) 341 + 342 + describe('realm.broadcast', () => { 343 + it('broadcasts message to other peers', async () => { 344 + const {client: client1, promise: promise1} = await createPeer() 345 + const {client: client2, promise: promise2} = await createPeer() 346 + 347 + // Client 1 receives peer-joined 348 + await client1.nextMessage() 349 + 350 + // Client 1 sends broadcast 351 + const broadcastEvent: RealmBroadcastEvent = { 352 + typ: 'evt', 353 + msg: 'realm.broadcast', 354 + dat: { 355 + payload: {message: 'hello world'}, 356 + recipients: false, 357 + }, 358 + } 359 + client1.send(JSON.stringify(broadcastEvent)) 360 + 361 + // Client 2 should receive it 362 + const message = await client2.nextMessage() 363 + const received = JSON.parse(message) 364 + 365 + expect(received).toMatchObject({ 366 + payload: {message: 'hello world'}, 367 + }) 368 + 369 + client1.close() 370 + client2.close() 371 + await promise1 372 + await promise2 373 + }, 2000) 374 + 375 + it('broadcasts to specific recipients', async () => { 376 + const {client: client1, promise: promise1} = await createPeer() 377 + const {client: client2, auth: auth2, promise: promise2} = await createPeer() 378 + const {client: client3, promise: promise3} = await createPeer() 379 + 380 + // Consume peer-joined events 381 + await client1.nextMessage() // peer2 joined 382 + await client1.nextMessage() // peer3 joined 383 + await client2.nextMessage() // peer3 joined 384 + 385 + // Client 1 sends broadcast only to client 2 386 + const broadcastEvent: RealmBroadcastEvent = { 387 + typ: 'evt', 388 + msg: 'realm.broadcast', 389 + dat: { 390 + payload: {message: 'direct message'}, 391 + recipients: [auth2.identid], 392 + }, 393 + } 394 + client1.send(JSON.stringify(broadcastEvent)) 395 + 396 + // Client 2 should receive it 397 + const message = await client2.nextMessage() 398 + const received = JSON.parse(message) 399 + expect(received).toMatchObject({ 400 + payload: {message: 'direct message'}, 401 + }) 402 + 403 + // Client 3 should not receive anything (we can't easily test this without timeouts) 404 + 405 + client1.close() 406 + client2.close() 407 + client3.close() 408 + await promise1 409 + await promise2 410 + await promise3 411 + }) 412 + }, 2000) 413 + 414 + describe('realm.signal', () => { 415 + it('relays signal to specific peer', async () => { 416 + const {client: client1, auth: auth1, promise: promise1} = await createPeer() 417 + const {client: client2, auth: auth2, promise: promise2} = await createPeer() 418 + 419 + // Client 1 receives peer-joined 420 + await client1.nextMessage() 421 + 422 + // Create a signed signal 423 + const keyPair = await generateSigningJwkPair() 424 + const signedPayload = {initiator: true} 425 + const signedJwt = await generateSignableJwt(signedPayload) 426 + .setIssuer(auth1.identid) 427 + .setAudience(auth2.identid) 428 + .sign(keyPair.privateKey) 429 + 430 + // Client 1 sends signal 431 + const signalEvent: RealmSignalEvent = { 432 + typ: 'evt', 433 + msg: 'realm.signal', 434 + dat: { 435 + localid: auth1.identid, 436 + remoteid: auth2.identid, 437 + signed: signedJwt, 438 + }, 439 + } 440 + client1.send(JSON.stringify(signalEvent)) 441 + 442 + // Client 2 receives signal 443 + const message = await client2.nextMessage() 444 + const received = jsonCodec(realmSignalEventSchema).safeParse(message) 445 + expect(received.success).toBe(true) 446 + 447 + // Verify the payload 448 + const payload = await jwtPayload(z.object({initiator: z.boolean()})).safeParseAsync( 449 + received.data?.dat.signed, 450 + ) 451 + expect(payload.success).toBe(true) 452 + expect(payload.data?.payload.initiator).toBe(true) 453 + 454 + client1.close() 455 + client2.close() 456 + await promise1 457 + await promise2 458 + }, 2000) 459 + }) 460 + 461 + describe('error handling', () => { 462 + it('handles malformed messages gracefully', async () => { 463 + const {client, promise} = await createPeer() 464 + 465 + client.send('not valid json') 466 + 467 + const message = await client.nextMessage() 468 + const error = jsonCodec(exceptionMessageSchema).safeParse(message) 469 + expect(error.success).toBe(true) 470 + 471 + client.close() 472 + await promise 473 + }, 2000) 474 + 475 + it('handles unknown message types', async () => { 476 + const {client, promise} = await createPeer() 477 + 478 + client.send( 479 + JSON.stringify({ 480 + typ: 'req', 481 + msg: 'realm.unknown', 482 + seq: 'unknown-1', 483 + dat: {}, 484 + }), 485 + ) 486 + 487 + const message = await client.nextMessage() 488 + const error = jsonCodec(exceptionMessageSchema).safeParse(message) 489 + expect(error.success).toBe(true) 490 + 491 + client.close() 492 + await promise 493 + }, 2000) 494 + }) 495 + 496 + describe('abort signal', () => { 497 + it('stops processing when aborted', async () => { 498 + const controller = new AbortController() 499 + const {client, promise} = await createPeer(controller.signal) 500 + 501 + controller.abort() 502 + 503 + await expect(promise).rejects.toThrow() 504 + 505 + // Clean up - socket may already be closed 506 + if (client.readyState === client.OPEN) { 507 + client.close() 508 + } 509 + }, 2000) 510 + }) 511 + })
+39
src/realm/protocol/spec/mock-action-store.ts
··· 1 + import {RealmAction} from '#realm/schema' 2 + import {IdentID} from '#realm/schema/brands' 3 + import {PeerClocks, Timestamp, explode} from '#realm/schema/timestamp' 4 + 5 + export class MockActionStore { 6 + #clocks: PeerClocks = {} 7 + #actions: RealmAction[] = [] 8 + 9 + buildSyncState(): Promise<PeerClocks> { 10 + return Promise.resolve({...this.#clocks}) 11 + } 12 + 13 + buildSyncDelta(remoteClocks: PeerClocks): Promise<RealmAction[]> { 14 + return Promise.resolve( 15 + // actions the remote doesn't have 16 + this.#actions.filter((action) => { 17 + const {identid} = explode(action.clk) 18 + const remoteClock = remoteClocks[identid] 19 + return !remoteClock || action.clk > remoteClock 20 + }), 21 + ) 22 + } 23 + 24 + setClock(identid: IdentID, timestamp: Timestamp) { 25 + this.#clocks[identid] = timestamp 26 + } 27 + 28 + addAction(action: RealmAction) { 29 + this.#actions.push(action) 30 + const {identid} = explode(action.clk) 31 + if (!this.#clocks[identid] || action.clk > this.#clocks[identid]) { 32 + this.#clocks[identid] = action.clk 33 + } 34 + } 35 + 36 + getActions(): RealmAction[] { 37 + return [...this.#actions] 38 + } 39 + }
+1 -1
src/realm/schema/timestamp.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 3 import {IdentBrand} from './brands' 4 4 import {compare, explode, generate, logicalClockSchema} from './timestamp'
+2 -2
src/realm/schema/timestamp.ts
··· 60 60 */ 61 61 export function compare(a?: Timestamp | null, b?: Timestamp | null): number { 62 62 if (a == null && b == null) return 0 63 - if (a == null) return 1 64 - if (b == null) return -1 63 + if (a == null) return -1 64 + if (b == null) return 1 65 65 66 66 const [_a, asec, acounter, aident] = a.split(':') 67 67 const [_b, bsec, bcounter, bident] = b.split(':')
+139 -172
src/realm/server/driver-preauth.spec.ts src/realm/protocol/preauth-protocol.spec.ts
··· 1 - import {describe, expect, it} from '@jest/globals' 1 + import {describe, expect, it} from 'vitest' 2 2 3 - import {mockSocketServer} from '#spec/helpers-socket' 3 + import {createSocketPair} from '#spec/helpers-socket-pair' 4 4 5 5 import {generateSignableJwt, generateSigningJwkPair, jwkExport} from '#lib/crypto/jwks' 6 6 import {jsonCodec} from '#lib/schema' ··· 12 12 preauthResSchema, 13 13 } from '#realm/protocol/messages-preauth' 14 14 import {IdentBrand, RealmBrand} from '#realm/schema/brands' 15 - 16 - import {drivePreauth} from './driver-preauth' 17 - import {ensureRealm, fetchRealm} from './storage' 18 - 19 - // Note: With jest-websocket-mock, the "server" is our test harness (WS), 20 - // and the "client" is what we're testing (the drivePreauth function). 21 - // This feels backwards because we're testing a server-side component. 22 - 23 - const socket = mockSocketServer() 15 + import {drivePreauth} from '#realm/server/driver-preauth' 16 + import {ensureRealm, fetchRealm} from '#realm/server/storage' 24 17 25 - describe('drivePreauth', () => { 18 + describe('preauth protocol', () => { 26 19 describe('preauth.register', () => { 27 - it('should successfully register a new realm', async () => { 28 - const ws = await socket.connect() 20 + it('registers a new realm', async () => { 21 + const [client, server] = createSocketPair() 29 22 const realmid = RealmBrand.generate() 30 23 const identid = IdentBrand.generate() 31 24 const keyPair = await generateSigningJwkPair() 32 25 33 - // Export public key as JWK 34 26 const pubkeyJwk = await jwkExport.parseAsync(keyPair.publicKey) 35 27 36 - // Create and sign the registration request 37 - const payload = { 28 + const payload: PreauthRegisterRequest = { 38 29 typ: 'req', 39 30 msg: 'preauth.register', 40 31 seq: 'sequence', 41 32 dat: { 42 33 pubkey: pubkeyJwk, 43 34 }, 44 - } satisfies PreauthRegisterRequest 35 + } 45 36 46 37 const jwt = await generateSignableJwt(payload) 47 38 .setAudience(realmid) ··· 49 40 .setJti('nonce') 50 41 .sign(keyPair.privateKey) 51 42 52 - // Start the preauth driver 53 - const authPromise = drivePreauth(ws) 43 + const authPromise = drivePreauth(server) 44 + client.send(jwt) 54 45 55 - // Send the JWT from the "server" (test harness) to "client" (code under test) 56 - socket.send(jwt) 57 - 58 - // Wait for response 59 - const response = await socket.nextMessage 46 + const response = await client.nextMessage() 60 47 const result = await authPromise 61 48 62 - // Verify the response 63 49 expect(result).not.toBeNull() 64 50 expect(result?.realm.realmid).toBe(realmid) 65 51 expect(result?.identid).toBe(identid) 66 52 67 53 const responseData = jsonCodec(preauthResSchema).safeParse(response) 68 - expect(responseData.success).toBeTruthy() 54 + expect(responseData.success).toBe(true) 69 55 expect(responseData.data).toMatchObject({ 70 56 typ: 'res', 71 57 msg: 'preauth.authn', ··· 77 63 }, 78 64 }) 79 65 80 - // Verify realm was created in storage 66 + // Verify realm was created 81 67 const realm = await fetchRealm(realmid) 82 68 expect(realm).not.toBeNull() 83 69 expect(realm?.realmid).toBe(realmid) 84 - }, 1000) 70 + }) 85 71 86 - it('should return existing realm if already registered', async () => { 87 - const ws = await socket.connect() 72 + it('returns existing realm if already registered', async () => { 73 + const [client, server] = createSocketPair() 88 74 const realmid = RealmBrand.generate() 89 75 const identid = IdentBrand.generate() 90 76 const keyPair = await generateSigningJwkPair() ··· 92 78 // Pre-create the realm 93 79 await ensureRealm(realmid, identid, keyPair.publicKey) 94 80 const pubkeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey) 95 - const payload = { 81 + 82 + const payload: PreauthRegisterRequest = { 96 83 typ: 'req', 97 84 msg: 'preauth.register', 98 85 seq: 'sequence', 99 86 dat: { 100 87 pubkey: pubkeyJwk, 101 88 }, 102 - } satisfies PreauthRegisterRequest 89 + } 103 90 104 91 const jwt = await generateSignableJwt(payload) 105 92 .setAudience(realmid) ··· 107 94 .setJti('nonce') 108 95 .sign(keyPair.privateKey) 109 96 110 - const authPromise = drivePreauth(ws) 111 - socket.send(jwt) 97 + const authPromise = drivePreauth(server) 98 + client.send(jwt) 112 99 113 - await socket.nextMessage 100 + await client.nextMessage() 114 101 const result = await authPromise 115 102 116 103 expect(result).not.toBeNull() 117 104 expect(result?.realm.realmid).toBe(realmid) 118 105 expect(result?.identid).toBe(identid) 119 - }, 1000) 106 + }) 120 107 }) 121 108 122 109 describe('preauth.authn', () => { 123 - it('should successfully authenticate an existing identity', async () => { 124 - const ws = await socket.connect() 110 + it('authenticates an existing identity', async () => { 111 + const [client, server] = createSocketPair() 125 112 const realmid = RealmBrand.generate() 126 113 const identid = IdentBrand.generate() 127 114 const keyPair = await generateSigningJwkPair() 128 115 const pubkeyJwk = await jwkExport.parseAsync(keyPair.publicKey) 129 116 130 - // Pre-create the realm and identity 131 117 await ensureRealm(realmid, identid, keyPair.publicKey) 132 118 133 - const payload = { 119 + const payload: PreauthAuthnRequest = { 134 120 typ: 'req', 135 121 msg: 'preauth.authn', 136 122 seq: 'sequence', 137 - } satisfies PreauthAuthnRequest 123 + } 138 124 139 125 const jwt = await generateSignableJwt(payload) 140 126 .setAudience(realmid) ··· 142 128 .setJti('nonce') 143 129 .sign(keyPair.privateKey) 144 130 145 - const authPromise = drivePreauth(ws) 146 - socket.send(jwt) 131 + const authPromise = drivePreauth(server) 132 + client.send(jwt) 147 133 148 - const response = await socket.nextMessage 134 + const response = await client.nextMessage() 149 135 const result = await authPromise 150 136 151 137 expect(result).not.toBeNull() 152 138 expect(result?.realm.realmid).toBe(realmid) 153 139 expect(result?.identid).toBe(identid) 154 - expect(result?.pubkey).toBeDefined() 155 140 156 141 const responseData = jsonCodec(preauthResSchema).safeParse(response) 157 - expect(responseData.success).toBeTruthy() 142 + expect(responseData.success).toBe(true) 158 143 expect(responseData.data).toMatchObject({ 159 144 typ: 'res', 160 145 msg: 'preauth.authn', 161 146 dat: expect.objectContaining({ 162 - peers: expect.arrayContaining([]), 147 + peers: [], 163 148 identities: expect.objectContaining({ 164 149 [identid]: pubkeyJwk, 165 150 }), 166 151 }), 167 152 }) 168 - }, 1000) 153 + }) 169 154 170 - it('should reject authentication for unknown realm', async () => { 171 - const ws = await socket.connect() 155 + it('rejects unknown realm', async () => { 156 + const [client, server] = createSocketPair() 172 157 const realmid = RealmBrand.generate() 173 158 const identid = IdentBrand.generate() 174 159 const keyPair = await generateSigningJwkPair() 175 160 176 - const payload = { 161 + const payload: PreauthAuthnRequest = { 177 162 typ: 'req', 178 163 msg: 'preauth.authn', 179 164 seq: 'sequence', 180 - } satisfies PreauthAuthnRequest 165 + } 181 166 182 167 const jwt = await generateSignableJwt(payload) 183 168 .setAudience(realmid) ··· 185 170 .setJti('nonce') 186 171 .sign(keyPair.privateKey) 187 172 188 - const authPromise = drivePreauth(ws) 189 - socket.send(jwt) 173 + const authPromise = drivePreauth(server) 174 + client.send(jwt) 190 175 191 - const response = await socket.nextMessage 176 + const response = await client.nextMessage() 192 177 const result = await authPromise 193 178 194 179 expect(result).toBeNull() 195 180 196 181 const responseData = jsonCodec(preauthResSchema).safeParse(response) 197 - expect(responseData.success).toBeTruthy() 182 + expect(responseData.success).toBe(true) 198 183 expect(responseData.data).toMatchObject({ 199 184 typ: 'err', 200 185 msg: 'preauth.authn', ··· 203 188 detail: '404 Not Found: unknown realm', 204 189 }, 205 190 }) 206 - }, 1000) 191 + }) 207 192 208 - it('should reject authentication for unknown identity', async () => { 209 - const ws = await socket.connect() 193 + it('rejects unknown identity', async () => { 194 + const [client, server] = createSocketPair() 210 195 const realmid = RealmBrand.generate() 211 196 const ownerIdentid = IdentBrand.generate() 212 197 const unknownIdentid = IdentBrand.generate() 213 198 const ownerKeyPair = await generateSigningJwkPair() 214 199 const unknownKeyPair = await generateSigningJwkPair() 215 200 216 - // Create realm with owner identity 217 201 await ensureRealm(realmid, ownerIdentid, ownerKeyPair.publicKey) 218 202 219 - // Try to authenticate with unknown identity 220 - const payload = { 203 + const payload: PreauthAuthnRequest = { 221 204 typ: 'req', 222 205 msg: 'preauth.authn', 223 206 seq: 'sequence', 224 - } satisfies PreauthAuthnRequest 207 + } 225 208 226 209 const jwt = await generateSignableJwt(payload) 227 210 .setAudience(realmid) ··· 229 212 .setJti('nonce') 230 213 .sign(unknownKeyPair.privateKey) 231 214 232 - const authPromise = drivePreauth(ws) 233 - socket.send(jwt) 215 + const authPromise = drivePreauth(server) 216 + client.send(jwt) 234 217 235 - const response = await socket.nextMessage 218 + const response = await client.nextMessage() 236 219 const result = await authPromise 237 220 238 221 expect(result).toBeNull() 239 222 240 223 const responseData = jsonCodec(preauthResSchema).safeParse(response) 241 - expect(responseData.success).toBeTruthy() 224 + expect(responseData.success).toBe(true) 242 225 expect(responseData.data).toMatchObject({ 243 226 typ: 'err', 244 227 msg: 'preauth.authn', ··· 247 230 detail: '401 Unauthorized: bad signature', 248 231 }, 249 232 }) 250 - }, 1000) 233 + }) 251 234 252 - it('should reject authentication with invalid signature', async () => { 253 - const ws = await socket.connect() 235 + it('rejects invalid signature', async () => { 236 + const [client, server] = createSocketPair() 254 237 const realmid = RealmBrand.generate() 255 238 const identid = IdentBrand.generate() 256 239 const keyPair = await generateSigningJwkPair() 257 240 const wrongKeyPair = await generateSigningJwkPair() 258 241 259 - // Create realm with correct identity 260 242 await ensureRealm(realmid, identid, keyPair.publicKey) 261 243 262 - // Sign with wrong private key 263 - const payload = { 244 + const payload: PreauthAuthnRequest = { 264 245 typ: 'req', 265 246 msg: 'preauth.authn', 266 247 seq: 'sequence', 267 - } satisfies PreauthAuthnRequest 248 + } 268 249 269 250 const jwt = await generateSignableJwt(payload) 270 251 .setAudience(realmid) ··· 272 253 .setJti('nonce') 273 254 .sign(wrongKeyPair.privateKey) 274 255 275 - const authPromise = drivePreauth(ws) 276 - socket.send(jwt) 256 + const authPromise = drivePreauth(server) 257 + client.send(jwt) 277 258 278 - const response = await socket.nextMessage 259 + const response = await client.nextMessage() 279 260 const result = await authPromise 280 261 281 262 expect(result).toBeNull() 282 263 283 264 const responseData = jsonCodec(preauthResSchema).safeParse(response) 284 - expect(responseData.success).toBeTruthy() 265 + expect(responseData.success).toBe(true) 285 266 expect(responseData.data).toMatchObject({ 286 267 typ: 'err', 287 268 msg: 'preauth.authn', ··· 290 271 detail: '401 Unauthorized: bad signature', 291 272 }, 292 273 }) 293 - }, 1000) 274 + }) 294 275 }) 295 276 296 277 describe('preauth.exchange', () => { 297 - it('should successfully exchange an invitation', async () => { 298 - const ws = await socket.connect() 278 + it('exchanges an invitation', async () => { 279 + const [client, server] = createSocketPair() 299 280 const realmid = RealmBrand.generate() 300 281 const inviterIdentid = IdentBrand.generate() 301 282 const inviteeIdentid = IdentBrand.generate() 302 283 const inviterKeyPair = await generateSigningJwkPair() 303 284 const inviteeKeyPair = await generateSigningJwkPair() 304 285 305 - // create realm with inviter identity 306 286 const realm = await ensureRealm(realmid, inviterIdentid, inviterKeyPair.publicKey) 307 287 const nonce = crypto.randomUUID() 308 288 309 - // create the invitation; payload doesn't matter, just claims 310 289 const inviteJwt = await generateSignableJwt({}) 311 290 .setAudience(realmid) 312 291 .setIssuer(inviterIdentid) 313 292 .setJti(nonce) 314 293 .sign(inviterKeyPair.privateKey) 315 294 316 - // create exchange request signed by invitee 317 295 const inviteePubkeyJwk = await jwkExport.parseAsync(inviteeKeyPair.publicKey) 318 - const payload = { 296 + const payload: PreauthExchangeInviteRequest = { 319 297 typ: 'req', 320 298 msg: 'preauth.exchange', 321 299 seq: 'sequence', ··· 323 301 pubkey: inviteePubkeyJwk, 324 302 inviteJwt: inviteJwt, 325 303 }, 326 - } satisfies PreauthExchangeInviteRequest 304 + } 327 305 328 306 const jwt = await generateSignableJwt(payload) 329 307 .setAudience(realmid) 330 308 .setIssuer(inviteeIdentid) 331 309 .sign(inviteeKeyPair.privateKey) 332 310 333 - const authPromise = drivePreauth(ws) 334 - socket.send(jwt) 311 + const authPromise = drivePreauth(server) 312 + client.send(jwt) 335 313 336 314 const result = await authPromise 337 315 expect(result).not.toBeNull() 338 316 expect(result?.realm.realmid).toBe(realmid) 339 317 expect(result?.identid).toBe(inviteeIdentid) 340 318 341 - const response = await socket.nextMessage 319 + const response = await client.nextMessage() 342 320 const responseData = jsonCodec(preauthResSchema).safeParse(response) 343 - expect(responseData.success).toBeTruthy() 321 + expect(responseData.success).toBe(true) 344 322 expect(responseData.data).toMatchObject({ 345 323 typ: 'res', 346 324 msg: 'preauth.authn', 347 325 dat: expect.objectContaining({ 348 - peers: expect.arrayContaining([]), 326 + peers: [], 349 327 identities: expect.objectContaining({ 350 328 [inviterIdentid]: expect.anything(), 351 329 }), 352 330 }), 353 331 }) 354 332 355 - // verify nonce was consumed 356 - const nonceValid = realm.validateNonce(nonce) 357 - expect(nonceValid).toBe(false) 358 - }, 1000) 333 + // Verify nonce was consumed 334 + expect(realm.validateNonce(nonce)).toBe(false) 335 + }) 359 336 360 - it('should reject invitation without nonce', async () => { 361 - const ws = await socket.connect() 337 + it('rejects invitation without nonce', async () => { 338 + const [client, server] = createSocketPair() 362 339 const realmid = RealmBrand.generate() 363 340 const inviterIdentid = IdentBrand.generate() 364 341 const inviteeIdentid = IdentBrand.generate() ··· 367 344 368 345 await ensureRealm(realmid, inviterIdentid, inviterKeyPair.publicKey) 369 346 370 - // create the invitation; payload doesn't matter, just claims 371 - // NO NONCE FOR THIS TEST 372 347 const inviteJwt = await generateSignableJwt({}) 373 348 .setAudience(realmid) 374 349 .setIssuer(inviterIdentid) 375 350 .sign(inviterKeyPair.privateKey) 376 351 377 352 const inviteePubkeyJwk = await crypto.subtle.exportKey('jwk', inviteeKeyPair.publicKey) 378 - const payload = { 353 + const payload: PreauthExchangeInviteRequest = { 379 354 typ: 'req', 380 355 msg: 'preauth.exchange', 381 356 seq: 'sequence', ··· 383 358 pubkey: inviteePubkeyJwk, 384 359 inviteJwt: inviteJwt, 385 360 }, 386 - } satisfies PreauthExchangeInviteRequest 361 + } 387 362 388 363 const jwt = await generateSignableJwt(payload) 389 364 .setAudience(realmid) 390 365 .setIssuer(inviteeIdentid) 391 366 .sign(inviteeKeyPair.privateKey) 392 367 393 - const authPromise = drivePreauth(ws) 394 - socket.send(jwt) 368 + const authPromise = drivePreauth(server) 369 + client.send(jwt) 395 370 396 - const response = await socket.nextMessage 371 + const response = await client.nextMessage() 397 372 const result = await authPromise 398 373 399 374 expect(result).toBeNull() 400 375 401 376 const responseData = jsonCodec(preauthResSchema).safeParse(response) 402 - expect(responseData.success).toBeTruthy() 377 + expect(responseData.success).toBe(true) 403 378 expect(responseData.data).toMatchObject({ 404 379 typ: 'err', 405 380 msg: 'preauth.authn', ··· 408 383 detail: '400 Bad Request: missing nonce', 409 384 }, 410 385 }) 411 - }, 1000) 386 + }) 412 387 413 - it('should reject invitation with reused nonce', async () => { 414 - const ws = await socket.connect() 388 + it('rejects reused nonce', async () => { 389 + const [client, server] = createSocketPair() 415 390 const realmid = RealmBrand.generate() 416 391 const inviterIdentid = IdentBrand.generate() 417 392 const inviteeIdentid = IdentBrand.generate() ··· 419 394 const inviteeKeyPair = await generateSigningJwkPair() 420 395 421 396 const realm = await ensureRealm(realmid, inviterIdentid, inviterKeyPair.publicKey) 422 - 423 - // consume the nonce 424 397 const nonce = crypto.randomUUID() 425 - realm.validateNonce(nonce) 398 + realm.validateNonce(nonce) // consume the nonce 426 399 427 - // create the invitation; payload doesn't matter, just claims 428 400 const inviteJwt = await generateSignableJwt({}) 429 401 .setAudience(realmid) 430 402 .setIssuer(inviterIdentid) ··· 432 404 .sign(inviterKeyPair.privateKey) 433 405 434 406 const inviteePubkeyJwk = await crypto.subtle.exportKey('jwk', inviteeKeyPair.publicKey) 435 - const payload = { 407 + const payload: PreauthExchangeInviteRequest = { 436 408 typ: 'req', 437 409 msg: 'preauth.exchange', 438 410 seq: 'sequence', ··· 440 412 pubkey: inviteePubkeyJwk, 441 413 inviteJwt: inviteJwt, 442 414 }, 443 - } satisfies PreauthExchangeInviteRequest 415 + } 444 416 445 417 const jwt = await generateSignableJwt(payload) 446 418 .setAudience(realmid) 447 419 .setIssuer(inviteeIdentid) 448 420 .sign(inviteeKeyPair.privateKey) 449 421 450 - const authPromise = drivePreauth(ws) 451 - socket.send(jwt) 422 + const authPromise = drivePreauth(server) 423 + client.send(jwt) 452 424 453 - const response = await socket.nextMessage 425 + const response = await client.nextMessage() 454 426 const result = await authPromise 455 427 456 428 expect(result).toBeNull() 457 429 458 430 const responseData = jsonCodec(preauthResSchema).safeParse(response) 459 - expect(responseData.success).toBeTruthy() 431 + expect(responseData.success).toBe(true) 460 432 expect(responseData.data).toMatchObject({ 461 433 typ: 'err', 462 434 msg: 'preauth.authn', ··· 465 437 detail: '403 Forbidden: nonce already seen', 466 438 }, 467 439 }) 468 - }, 1000) 440 + }) 469 441 470 - it('should reject invitation from unknown identity', async () => { 471 - const ws = await socket.connect() 442 + it('rejects invitation from unknown identity', async () => { 443 + const [client, server] = createSocketPair() 472 444 const realmid = RealmBrand.generate() 473 445 const ownerIdentid = IdentBrand.generate() 474 446 const unknownInviterIdentid = IdentBrand.generate() ··· 476 448 const ownerKeyPair = await generateSigningJwkPair() 477 449 const unknownInviterKeyPair = await generateSigningJwkPair() 478 450 const inviteeKeyPair = await generateSigningJwkPair() 479 - const nonce = crypto.randomUUID() 480 451 481 - // create realm with owner only 482 452 await ensureRealm(realmid, ownerIdentid, ownerKeyPair.publicKey) 453 + const nonce = crypto.randomUUID() 483 454 484 - // create the invitation; payload doesn't matter, just claims 485 455 const inviteJwt = await generateSignableJwt({}) 486 456 .setAudience(realmid) 487 457 .setIssuer(unknownInviterIdentid) ··· 489 459 .sign(unknownInviterKeyPair.privateKey) 490 460 491 461 const inviteePubkeyJwk = await crypto.subtle.exportKey('jwk', inviteeKeyPair.publicKey) 492 - const payload = { 462 + const payload: PreauthExchangeInviteRequest = { 493 463 typ: 'req', 494 464 msg: 'preauth.exchange', 495 465 seq: 'sequence', ··· 497 467 pubkey: inviteePubkeyJwk, 498 468 inviteJwt: inviteJwt, 499 469 }, 500 - } satisfies PreauthExchangeInviteRequest 470 + } 501 471 502 472 const jwt = await generateSignableJwt(payload) 503 473 .setAudience(realmid) 504 474 .setIssuer(inviteeIdentid) 505 475 .sign(inviteeKeyPair.privateKey) 506 476 507 - const authPromise = drivePreauth(ws) 508 - socket.send(jwt) 477 + const authPromise = drivePreauth(server) 478 + client.send(jwt) 509 479 510 - const response = await socket.nextMessage 480 + const response = await client.nextMessage() 511 481 const result = await authPromise 512 482 513 483 expect(result).toBeNull() 514 484 515 485 const responseData = jsonCodec(preauthResSchema).safeParse(response) 516 - expect(responseData.success).toBeTruthy() 486 + expect(responseData.success).toBe(true) 517 487 expect(responseData.data).toMatchObject({ 518 488 typ: 'err', 519 489 msg: 'preauth.authn', ··· 522 492 detail: '401 Unauthorized: bad signature', 523 493 }, 524 494 }) 525 - }, 1000) 495 + }) 526 496 527 - it('should reject invitation with invalid signature', async () => { 528 - const ws = await socket.connect() 497 + it('rejects invitation with invalid signature', async () => { 498 + const [client, server] = createSocketPair() 529 499 const realmid = RealmBrand.generate() 530 500 const inviterIdentid = IdentBrand.generate() 531 501 const inviteeIdentid = IdentBrand.generate() ··· 536 506 await ensureRealm(realmid, inviterIdentid, inviterKeyPair.publicKey) 537 507 const nonce = crypto.randomUUID() 538 508 539 - // create the invitation; payload doesn't matter, just claims 540 509 const inviteJwt = await generateSignableJwt({}) 541 510 .setAudience(realmid) 542 511 .setIssuer(inviterIdentid) ··· 544 513 .sign(wrongKeyPair.privateKey) 545 514 546 515 const inviteePubkeyJwk = await crypto.subtle.exportKey('jwk', inviteeKeyPair.publicKey) 547 - const payload = { 516 + const payload: PreauthExchangeInviteRequest = { 548 517 typ: 'req', 549 518 msg: 'preauth.exchange', 550 519 seq: 'sequence', ··· 552 521 pubkey: inviteePubkeyJwk, 553 522 inviteJwt: inviteJwt, 554 523 }, 555 - } satisfies PreauthExchangeInviteRequest 524 + } 556 525 557 526 const jwt = await generateSignableJwt(payload) 558 527 .setAudience(realmid) 559 528 .setIssuer(inviteeIdentid) 560 529 .sign(inviteeKeyPair.privateKey) 561 530 562 - const authPromise = drivePreauth(ws) 563 - socket.send(jwt) 531 + const authPromise = drivePreauth(server) 532 + client.send(jwt) 564 533 565 - const response = await socket.nextMessage 534 + const response = await client.nextMessage() 566 535 const result = await authPromise 567 536 568 537 expect(result).toBeNull() 569 538 570 539 const responseData = jsonCodec(preauthResSchema).safeParse(response) 571 - expect(responseData.success).toBeTruthy() 540 + expect(responseData.success).toBe(true) 572 541 expect(responseData.data).toMatchObject({ 573 542 typ: 'err', 574 543 msg: 'preauth.authn', ··· 577 546 detail: '401 Unauthorized: bad signature', 578 547 }, 579 548 }) 580 - }, 1000) 549 + }) 581 550 582 - it('should reject exchange for unknown realm', async () => { 583 - const ws = await socket.connect() 551 + it('rejects exchange for unknown realm', async () => { 552 + const [client, server] = createSocketPair() 584 553 const realmid = RealmBrand.generate() 585 554 const inviterIdentid = IdentBrand.generate() 586 555 const inviteeIdentid = IdentBrand.generate() 587 556 const inviterKeyPair = await generateSigningJwkPair() 588 557 const inviteeKeyPair = await generateSigningJwkPair() 589 558 590 - // don't create realm 591 559 const nonce = crypto.randomUUID() 592 560 593 - // create the invitation; payload doesn't matter, just claims 594 561 const inviteJwt = await generateSignableJwt({}) 595 562 .setAudience(realmid) 596 563 .setIssuer(inviterIdentid) ··· 598 565 .sign(inviterKeyPair.privateKey) 599 566 600 567 const inviteePubkeyJwk = await crypto.subtle.exportKey('jwk', inviteeKeyPair.publicKey) 601 - const payload = { 568 + const payload: PreauthExchangeInviteRequest = { 602 569 typ: 'req', 603 570 msg: 'preauth.exchange', 604 571 seq: 'sequence', ··· 606 573 pubkey: inviteePubkeyJwk, 607 574 inviteJwt: inviteJwt, 608 575 }, 609 - } satisfies PreauthExchangeInviteRequest 576 + } 610 577 611 578 const jwt = await generateSignableJwt(payload) 612 579 .setAudience(realmid) 613 580 .setIssuer(inviteeIdentid) 614 581 .sign(inviteeKeyPair.privateKey) 615 582 616 - const authPromise = drivePreauth(ws) 617 - socket.send(jwt) 583 + const authPromise = drivePreauth(server) 584 + client.send(jwt) 618 585 619 - const response = await socket.nextMessage 586 + const response = await client.nextMessage() 620 587 const result = await authPromise 621 588 622 589 expect(result).toBeNull() 623 590 624 591 const responseData = jsonCodec(preauthResSchema).safeParse(response) 625 - expect(responseData.success).toBeTruthy() 592 + expect(responseData.success).toBe(true) 626 593 expect(responseData.data).toMatchObject({ 627 594 typ: 'err', 628 595 msg: 'preauth.authn', ··· 631 598 detail: '404 Not Found: unknown realm', 632 599 }, 633 600 }) 634 - }, 1000) 601 + }) 635 602 }) 636 603 637 - describe('timeout handling', () => { 638 - it('should timeout if no message received within timeout', async () => { 639 - const ws = await socket.connect() 640 - const authPromise = drivePreauth(ws, 250) 604 + describe('timeout and abort', () => { 605 + it('times out if no message received', async () => { 606 + const [, server] = createSocketPair() 607 + const authPromise = drivePreauth(server, 250) 641 608 await expect(authPromise).rejects.toThrow() 642 - }, 500) 609 + }) 643 610 644 - it('should respect external abort signal', async () => { 645 - const ws = await socket.connect() 611 + it('respects abort signal', async () => { 612 + const [, server] = createSocketPair() 646 613 const controller = new AbortController() 647 - const authPromise = drivePreauth(ws, undefined, controller.signal) 614 + const authPromise = drivePreauth(server, undefined, controller.signal) 648 615 649 616 setTimeout(() => { 650 617 controller.abort() 651 618 }, 100) 652 619 await expect(authPromise).rejects.toThrow() 653 - }, 500) 620 + }) 654 621 }) 655 622 656 623 describe('malformed requests', () => { 657 - it('should reject non-JWT messages', async () => { 658 - const ws = await socket.connect() 659 - const authPromise = drivePreauth(ws) 624 + it('rejects non-JWT messages', async () => { 625 + const [client, server] = createSocketPair() 626 + const authPromise = drivePreauth(server) 660 627 661 - socket.send('not a jwt') 628 + client.send('not a jwt') 662 629 663 630 await expect(authPromise).rejects.toThrow() 664 - }, 1000) 631 + }) 665 632 666 - it('should reject JWT with invalid payload schema', async () => { 667 - const ws = await socket.connect() 633 + it('rejects JWT with invalid payload', async () => { 634 + const [client, server] = createSocketPair() 668 635 const realmid = RealmBrand.generate() 669 636 const identid = IdentBrand.generate() 670 637 const keyPair = await generateSigningJwkPair() ··· 674 641 .setIssuer(identid) 675 642 .sign(keyPair.privateKey) 676 643 677 - const authPromise = drivePreauth(ws) 678 - socket.send(jwt) 644 + const authPromise = drivePreauth(server) 645 + client.send(jwt) 679 646 680 647 await expect(authPromise).rejects.toThrow() 681 - }, 1000) 648 + }) 682 649 }) 683 650 })
-472
src/realm/server/driver-realm.spec.ts
··· 1 - import {afterEach, beforeEach, describe, expect, it} from '@jest/globals' 2 - import {WebSocket} from 'isomorphic-ws' 3 - import {z} from 'zod/v4' 4 - 5 - import {mockSocketServer} from '#spec/helpers-socket' 6 - 7 - import {sleep} from '#lib/async/sleep' 8 - import {generateSignableJwt, generateSigningJwkPair, jwkExport} from '#lib/crypto/jwks' 9 - import {jwtPayload} from '#lib/crypto/jwts' 10 - import {exceptionMessageSchema, jsonCodec} from '#lib/schema' 11 - 12 - import { 13 - RealmAnnounceRequest, 14 - RealmBroadcastEvent, 15 - RealmPeerLeftEvent, 16 - RealmPingRequest, 17 - RealmPingResponse, 18 - RealmSignalEvent, 19 - realmAnnounceResponseSchema, 20 - realmPeerLeftEventSchema, 21 - realmPingResponseSchema, 22 - realmSignalEventSchema, 23 - } from '#realm/protocol/messages-realm' 24 - import {RealmAction, actionSchema} from '#realm/schema' 25 - import {IdentBrand, RealmBrand, RealmID} from '#realm/schema/brands' 26 - import {generate as generateTimestamp} from '#realm/schema/timestamp' 27 - 28 - import {AuthenticatedIdentity} from './driver-preauth' 29 - import {driveRealm} from './driver-realm' 30 - import {Realm} from './realm' 31 - 32 - const socket = mockSocketServer() 33 - 34 - async function createAuthenticatedConnection( 35 - realm: Realm, 36 - signal?: AbortSignal, 37 - ): Promise<{ws: WebSocket; auth: AuthenticatedIdentity; promise: Promise<unknown>}> { 38 - const identid = IdentBrand.generate() 39 - const keyPair = await generateSigningJwkPair() 40 - const pubkey = keyPair.publicKey 41 - const pubjwk = await jwkExport.parseAsync(pubkey) 42 - await realm.admitIdentity(identid, pubkey) 43 - 44 - const ws = await socket.connect() 45 - realm.attachSocket(identid, ws) 46 - 47 - const auth = {realm, identid, pubkey, pubjwk} 48 - const promise = driveRealm(ws, auth, signal) 49 - 50 - return {ws, auth, promise} 51 - } 52 - 53 - describe('driveRealm', () => { 54 - let realm: Realm 55 - let realmid: RealmID 56 - 57 - beforeEach(() => { 58 - realmid = RealmBrand.generate() 59 - realm = new Realm(realmid, ':memory:') 60 - }) 61 - 62 - afterEach(() => { 63 - realm.shutdown() 64 - }) 65 - 66 - describe('connection lifecycle', () => { 67 - it('should broadcast peer-left event when connection closes', async () => { 68 - const {ws: ws1, auth: auth1, promise: promise1} = await createAuthenticatedConnection(realm) 69 - const {ws: ws2, promise: promise2} = await createAuthenticatedConnection(realm) 70 - await socket.nextMessage // peer2 -> 1 71 - 72 - // close first connection 73 - ws1.close() 74 - await promise1 75 - 76 - // second peer should receive peer-left event 77 - const message = await socket.nextMessage 78 - const event = jsonCodec(realmPeerLeftEventSchema).parse(message) 79 - expect(event).toMatchObject({ 80 - typ: 'evt', 81 - msg: 'realm.peer-left', 82 - dat: { 83 - identid: auth1.identid, 84 - }, 85 - } satisfies RealmPeerLeftEvent) 86 - 87 - // cleanup 88 - ws2.close() 89 - await promise2 90 - }, 1000) 91 - }) 92 - 93 - describe('realm.ping', () => { 94 - it('should respond to ping request with current clocks', async () => { 95 - const {ws, auth, promise} = await createAuthenticatedConnection(realm) 96 - 97 - // record some actions to populate clocks 98 - const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 99 - realm.recordActions([ 100 - { 101 - typ: 'act', 102 - msg: 'test.action', 103 - clk: timestamp, 104 - dat: {value: 1}, 105 - } satisfies RealmAction, 106 - ]) 107 - 108 - const pingRequest: RealmPingRequest = { 109 - typ: 'req', 110 - msg: 'realm.ping', 111 - seq: 'ping-1', 112 - dat: { 113 - clocks: {}, 114 - requestsSync: false, 115 - }, 116 - } 117 - socket.send(JSON.stringify(pingRequest)) 118 - 119 - // check response 120 - 121 - const message = await socket.nextMessage 122 - const response = jsonCodec(realmPingResponseSchema).parse(message) 123 - expect(response).toMatchObject({ 124 - typ: 'res', 125 - msg: 'realm.ping', 126 - seq: 'ping-1', 127 - dat: { 128 - clocks: { 129 - [auth.identid]: timestamp, 130 - }, 131 - }, 132 - } satisfies RealmPingResponse) 133 - 134 - ws.close() 135 - await promise 136 - }, 1000) 137 - 138 - it('should send sync delta when ping requests sync', async () => { 139 - const {ws, auth, promise} = await createAuthenticatedConnection(realm) 140 - const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 141 - realm.recordActions([ 142 - { 143 - typ: 'act', 144 - msg: 'test.action', 145 - clk: timestamp, 146 - dat: {value: 1}, 147 - } satisfies RealmAction, 148 - ]) 149 - 150 - const pingRequest: RealmPingRequest = { 151 - typ: 'req', 152 - msg: 'realm.ping', 153 - seq: 'ping-sync', 154 - dat: { 155 - clocks: {}, 156 - requestsSync: true, 157 - }, 158 - } 159 - socket.send(JSON.stringify(pingRequest)) 160 - 161 - // should receive ping response 162 - const pingResponseMessage = await socket.nextMessage 163 - const pingResponse = jsonCodec(realmPingResponseSchema).safeParse(pingResponseMessage) 164 - expect(pingResponse.success).toBeTruthy() 165 - 166 - // should also receive sync delta (array of actions) 167 - const syncDeltaMessage = await socket.nextMessage 168 - const syncDeltaResponse = jsonCodec(z.array(actionSchema)).safeParse(syncDeltaMessage) 169 - expect(syncDeltaResponse.success).toBeTruthy() 170 - expect(syncDeltaResponse.data).toHaveLength(1) 171 - expect(syncDeltaResponse.data).toContainEqual({ 172 - typ: 'act', 173 - msg: 'test.action', 174 - clk: timestamp, 175 - dat: {value: 1}, 176 - }) 177 - 178 - ws.close() 179 - await promise 180 - }, 1000) 181 - }) 182 - 183 - describe('realm.announce', () => { 184 - it('should respond to announce request with server info', async () => { 185 - const {ws, promise} = await createAuthenticatedConnection(realm) 186 - 187 - // send announce request 188 - const announceRequest: RealmAnnounceRequest = { 189 - typ: 'req', 190 - msg: 'realm.announce', 191 - seq: 'announce-1', 192 - dat: { 193 - clocks: {}, 194 - requestsSync: false, 195 - deviceCaps: { 196 - corsFetch: false, 197 - networkQuality: 3, 198 - }, 199 - deviceInfo: { 200 - name: 'test-client', 201 - ua: 'test-ua', 202 - }, 203 - }, 204 - } 205 - socket.send(JSON.stringify(announceRequest)) 206 - 207 - // should receive announce response 208 - const message = await socket.nextMessage 209 - const response = jsonCodec(realmAnnounceResponseSchema).parse(message) 210 - 211 - expect(response).toMatchObject({ 212 - typ: 'res', 213 - msg: 'realm.announce', 214 - seq: 'announce-1', 215 - dat: expect.objectContaining({ 216 - clocks: expect.any(Object), 217 - deviceCaps: { 218 - corsFetch: true, 219 - networkQuality: 5, 220 - }, 221 - deviceInfo: expect.objectContaining({ 222 - name: expect.any(String), 223 - ua: expect.any(String), 224 - }), 225 - }), 226 - }) 227 - 228 - ws.close() 229 - await promise 230 - }, 1000) 231 - 232 - it('should send sync delta when announce requests sync', async () => { 233 - const {ws, auth, promise} = await createAuthenticatedConnection(realm) 234 - const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 235 - realm.recordActions([ 236 - { 237 - typ: 'act', 238 - msg: 'test.action', 239 - clk: timestamp, 240 - dat: {value: 1}, 241 - } satisfies RealmAction, 242 - ]) 243 - 244 - const announceRequest: RealmAnnounceRequest = { 245 - typ: 'req', 246 - msg: 'realm.announce', 247 - seq: 'announce-sync', 248 - dat: { 249 - clocks: {}, 250 - requestsSync: true, 251 - }, 252 - } 253 - socket.send(JSON.stringify(announceRequest)) 254 - 255 - // Should receive announce response 256 - const announceMessage = await socket.nextMessage 257 - const announceResponse = jsonCodec(realmAnnounceResponseSchema).safeParse(announceMessage) 258 - expect(announceResponse.success).toBeTruthy() 259 - 260 - // Should also receive sync delta 261 - const syncMessage = await socket.nextMessage 262 - const syncResponse = jsonCodec(z.array(actionSchema)).safeParse(syncMessage) 263 - expect(syncResponse.success).toBeTruthy() 264 - expect(syncResponse.data).toHaveLength(1) 265 - 266 - ws.close() 267 - await promise 268 - }, 1000) 269 - }) 270 - 271 - describe('action recording', () => { 272 - it('should record actions sent by client', async () => { 273 - const {ws, auth, promise} = await createAuthenticatedConnection(realm) 274 - 275 - // send actions 276 - const timestamp = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 277 - const actions: RealmAction[] = [ 278 - { 279 - typ: 'act', 280 - msg: 'test.action', 281 - clk: timestamp, 282 - dat: {value: 1}, 283 - }, 284 - ] 285 - socket.send(JSON.stringify(actions)) 286 - 287 - // verify actions were recorded 288 - await sleep(50) // let it flow 289 - const syncState = realm.buildSyncState() 290 - expect(syncState[auth.identid]).toBe(timestamp) 291 - 292 - ws.close() 293 - await promise 294 - }, 1000) 295 - 296 - it('should handle multiple actions in a batch', async () => { 297 - const {ws, auth, promise} = await createAuthenticatedConnection(realm) 298 - 299 - // send multiple actions 300 - const ts1 = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 0) 301 - const ts2 = generateTimestamp(auth.identid, Math.floor(Date.now() / 1000), 1) 302 - const actions: RealmAction[] = [ 303 - {typ: 'act', msg: 'test.action1', clk: ts1, dat: {value: 1}}, 304 - {typ: 'act', msg: 'test.action2', clk: ts2, dat: {value: 2}}, 305 - ] 306 - socket.send(JSON.stringify(actions)) 307 - 308 - // Give it a moment to process 309 - await new Promise((resolve) => setTimeout(resolve, 50)) 310 - 311 - // verify both actions were recorded 312 - const syncState = realm.buildSyncState() 313 - expect(syncState[auth.identid]).toBe(ts2) 314 - 315 - const delta = realm.buildSyncDelta({}) 316 - expect(delta).toHaveLength(2) 317 - 318 - ws.close() 319 - await promise 320 - }, 1000) 321 - }) 322 - 323 - describe('realm.broadcast', () => { 324 - it('should broadcast message to other peers', async () => { 325 - const {ws: ws1, promise: promise1} = await createAuthenticatedConnection(realm) 326 - const {ws: ws2, promise: promise2} = await createAuthenticatedConnection(realm) 327 - await socket.nextMessage // peer-joined (peer2 -> peer1) 328 - 329 - // send broadcast from peer1 330 - const broadcastEvent: RealmBroadcastEvent = { 331 - typ: 'evt', 332 - msg: 'realm.broadcast', 333 - dat: { 334 - payload: {message: 'hello world'}, 335 - recipients: false, // broadcast to all except sender 336 - }, 337 - } 338 - socket.send(JSON.stringify(broadcastEvent)) 339 - await sleep(50) 340 - 341 - // TODO: make sure we only get one more message on the socket 342 - 343 - ws1.close() 344 - ws2.close() 345 - await promise1 346 - await promise2 347 - }, 1000) 348 - 349 - it('should broadcast to specific recipients', async () => { 350 - const {ws: ws1, promise: promise1} = await createAuthenticatedConnection(realm) 351 - const {ws: ws2, auth: auth2, promise: promise2} = await createAuthenticatedConnection(realm) 352 - await socket.nextMessage // peer joined -> 1 353 - 354 - const {ws: ws3, promise: promise3} = await createAuthenticatedConnection(realm) 355 - await socket.nextMessage // peer joined -> 2 356 - await socket.nextMessage // peer joined -> 1 357 - 358 - // Send broadcast to specific recipient 359 - const broadcastEvent: RealmBroadcastEvent = { 360 - typ: 'evt', 361 - msg: 'realm.broadcast', 362 - dat: { 363 - payload: {message: 'direct message'}, 364 - recipients: [auth2.identid], 365 - }, 366 - } 367 - 368 - socket.send(JSON.stringify(broadcastEvent)) 369 - await sleep(50) 370 - 371 - // TODO: make sure there's only one message on the server 372 - 373 - ws1.close() 374 - ws2.close() 375 - ws3.close() 376 - await promise1 377 - await promise2 378 - await promise3 379 - }, 1000) 380 - }) 381 - 382 - describe('realm.signal', () => { 383 - it('should relay signal to specific peer', async () => { 384 - const {ws: ws1, auth: auth1, promise: promise1} = await createAuthenticatedConnection(realm) 385 - const {ws: ws2, auth: auth2, promise: promise2} = await createAuthenticatedConnection(realm) 386 - await socket.nextMessage // peer joined -> 1 387 - 388 - // create a signed signal 389 - const keyPair = await generateSigningJwkPair() 390 - const signedPayload = {initiator: true} 391 - const signedJwt = await generateSignableJwt(signedPayload) 392 - .setIssuer(auth1.identid) 393 - .setAudience(auth2.identid) 394 - .sign(keyPair.privateKey) 395 - 396 - // send signal event 397 - const signalEvent = { 398 - typ: 'evt', 399 - msg: 'realm.signal', 400 - dat: { 401 - localid: auth1.identid, 402 - remoteid: auth2.identid, 403 - signed: signedJwt, 404 - }, 405 - } satisfies RealmSignalEvent 406 - socket.send(JSON.stringify(signalEvent)) 407 - await sleep(50) 408 - 409 - // we got the signal 410 - const signalData = await socket.nextMessage 411 - const signalMessage = jsonCodec(realmSignalEventSchema).safeParse(signalData) 412 - expect(signalMessage.success).toBeTruthy() 413 - 414 - // and it's got the payload in the signed jwt 415 - const message = await jwtPayload(z.object({initiator: z.boolean()})).safeParseAsync( 416 - signalMessage.data?.dat.signed, 417 - ) 418 - expect(message.success).toBeTruthy() 419 - expect(message.data?.payload.initiator).toBe(true) 420 - 421 - ws1.close() 422 - ws2.close() 423 - await promise1 424 - await promise2 425 - }, 1000) 426 - }) 427 - 428 - describe('error handling', () => { 429 - it('should handle malformed messages gracefully', async () => { 430 - const {ws, promise} = await createAuthenticatedConnection(realm) 431 - 432 - socket.send('not valid json') 433 - 434 - const errorMessage = await socket.nextMessage 435 - const errorResult = jsonCodec(exceptionMessageSchema).safeParse(errorMessage) 436 - expect(errorResult.success).toBeTruthy() 437 - 438 - ws.close() 439 - await promise 440 - }, 1000) 441 - 442 - it('should handle unknown message types', async () => { 443 - const {ws, promise} = await createAuthenticatedConnection(realm) 444 - 445 - socket.send( 446 - JSON.stringify({ 447 - typ: 'req', 448 - msg: 'realm.unknown', 449 - seq: 'unknown-1', 450 - dat: {}, 451 - }), 452 - ) 453 - 454 - const errorMessage = await socket.nextMessage 455 - const errorResult = jsonCodec(exceptionMessageSchema).safeParse(errorMessage) 456 - expect(errorResult.success).toBeTruthy() 457 - 458 - ws.close() 459 - await promise 460 - }, 1000) 461 - }) 462 - 463 - describe('abort signal handling', () => { 464 - it('should stop processing when abort signal is triggered', async () => { 465 - const controller = new AbortController() 466 - const {promise} = await createAuthenticatedConnection(realm, controller.signal) 467 - 468 - controller.abort() 469 - await expect(promise).rejects.toThrow() 470 - }, 1000) 471 - }) 472 - })
+1 -1
src/realm/server/realm.spec.ts
··· 1 - import {beforeEach, describe, expect, it} from '@jest/globals' 2 1 import {WebSocket} from 'isomorphic-ws' 2 + import {beforeEach, describe, expect, it} from 'vitest' 3 3 4 4 import {RealmAction} from '#realm/schema' 5 5 import {IdentBrand, RealmBrand} from '#realm/schema/brands'
+1 -1
src/realm/server/realm.ts
··· 242 242 const schema = jsonCodec(actionSchema) 243 243 244 244 // a null clock is the same as no clock 245 - const entries = Object.entries(clocks).filter(([_, v]) => !!v) 245 + const entries = Object.entries(clocks).filter(([, v]) => !!v) 246 246 if (entries.length === 0) { 247 247 const stmt = this.#database.prepare(`SELECT action FROM action ORDER BY clock ASC`) 248 248 const rows = stmt.all()
+1 -1
src/realm/server/storage.spec.ts
··· 1 - import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/globals' 2 1 import fs from 'node:fs/promises' 3 2 import os from 'node:os' 4 3 import path from 'node:path' 4 + import {afterEach, beforeAll, beforeEach, describe, expect, it} from 'vitest' 5 5 6 6 import {IdentBrand, RealmBrand} from '#realm/schema/brands' 7 7
+240
src/spec/helpers-socket-pair.spec.ts
··· 1 + import {describe, expect, it} from 'vitest' 2 + 3 + import {sleep} from '#lib/async/sleep' 4 + 5 + import {createChannelPair, createSocketPair} from './helpers-socket-pair' 6 + 7 + describe('createSocketPair', () => { 8 + it('sends messages between paired sockets', async () => { 9 + const [client, server] = createSocketPair() 10 + await sleep(10) // let the sockets open 11 + 12 + // Client sends, server receives 13 + client.send('hello from client') 14 + const serverMsg = await server.nextMessage() 15 + expect(serverMsg).toBe('hello from client') 16 + 17 + // Server sends, client receives 18 + server.send('hello from server') 19 + const clientMsg = await client.nextMessage() 20 + expect(clientMsg).toBe('hello from server') 21 + }, 1000) 22 + 23 + it('emits message events', async () => { 24 + const [client, server] = createSocketPair() 25 + await sleep(10) // let the sockets open 26 + 27 + const messages: string[] = [] 28 + server.addEventListener('message', (event) => { 29 + messages.push((event as {data: string}).data) 30 + }) 31 + 32 + client.send('message 1') 33 + client.send('message 2') 34 + 35 + // Wait for messages to be delivered 36 + await server.nextMessage() 37 + await server.nextMessage() 38 + 39 + expect(messages).toEqual(['message 1', 'message 2']) 40 + }, 1000) 41 + 42 + it('handles close from client', async () => { 43 + const [client, server] = createSocketPair() 44 + 45 + let serverClosed = false 46 + server.addEventListener('close', () => { 47 + serverClosed = true 48 + }) 49 + 50 + client.close() 51 + await sleep(10) // let the sockets close 52 + 53 + expect(client.readyState).toBe(client.CLOSED) 54 + expect(server.readyState).toBe(server.CLOSED) 55 + expect(serverClosed).toBe(true) 56 + }, 1000) 57 + 58 + it('handles close from server', async () => { 59 + const [client, server] = createSocketPair() 60 + 61 + let clientClosed = false 62 + client.addEventListener('close', () => { 63 + clientClosed = true 64 + }) 65 + 66 + server.close() 67 + await sleep(10) // let the sockets close 68 + 69 + expect(client.readyState).toBe(client.CLOSED) 70 + expect(server.readyState).toBe(server.CLOSED) 71 + expect(clientClosed).toBe(true) 72 + }) 73 + 74 + it('queues messages until nextMessage is called', async () => { 75 + const [client, server] = createSocketPair() 76 + await sleep(0) // let them start 77 + 78 + // Send multiple messages before awaiting 79 + client.send('first') 80 + client.send('second') 81 + client.send('third') 82 + await sleep(0) 83 + 84 + expect(await server.nextMessage()).toBe('first') 85 + expect(await server.nextMessage()).toBe('second') 86 + expect(await server.nextMessage()).toBe('third') 87 + }) 88 + }) 89 + 90 + describe('createChannelPair', () => { 91 + it('sends messages between paired channels', async () => { 92 + const [initiator, responder] = createChannelPair(true) 93 + 94 + // Initiator sends, responder receives 95 + initiator.send('hello from initiator') 96 + const responderMsg = await responder.nextMessage() 97 + expect(responderMsg).toBe('hello from initiator') 98 + 99 + // Responder sends, initiator receives 100 + responder.send('hello from responder') 101 + const initiatorMsg = await initiator.nextMessage() 102 + expect(initiatorMsg).toBe('hello from responder') 103 + }) 104 + 105 + it('emits data events', async () => { 106 + const [initiator, responder] = createChannelPair(true) 107 + 108 + const messages: (string | ArrayBuffer)[] = [] 109 + responder.addEventListener('data', (event) => { 110 + messages.push(event.detail) 111 + }) 112 + 113 + initiator.send('message 1') 114 + initiator.send('message 2') 115 + 116 + // Wait for messages to be delivered 117 + await responder.nextMessage() 118 + await responder.nextMessage() 119 + 120 + expect(messages).toEqual(['message 1', 'message 2']) 121 + }) 122 + 123 + it('emits connect events on both sides', async () => { 124 + const [initiator, responder] = createChannelPair() 125 + 126 + let initiatorConnected = false 127 + let responderConnected = false 128 + 129 + initiator.addEventListener('connect', () => { 130 + initiatorConnected = true 131 + }) 132 + responder.addEventListener('connect', () => { 133 + responderConnected = true 134 + }) 135 + 136 + initiator.connect() 137 + 138 + // Wait for connect to propagate 139 + await sleep(10) // let connection propagate 140 + await new Promise((resolve) => setTimeout(resolve, 10)) 141 + 142 + expect(initiatorConnected).toBe(true) 143 + expect(responderConnected).toBe(true) 144 + expect(initiator.connected).toBe(true) 145 + expect(responder.connected).toBe(true) 146 + }) 147 + 148 + it('handles destroy from initiator', async () => { 149 + const [initiator, responder] = createChannelPair(true) 150 + 151 + let responderClosed = false 152 + responder.addEventListener('close', () => { 153 + responderClosed = true 154 + }) 155 + 156 + initiator.destroy() 157 + 158 + // Wait for close to propagate 159 + await new Promise((resolve) => setTimeout(resolve, 10)) 160 + 161 + expect(initiator.connected).toBe(false) 162 + expect(responder.connected).toBe(false) 163 + expect(responderClosed).toBe(true) 164 + }) 165 + 166 + it('handles destroy from responder', async () => { 167 + const [initiator, responder] = createChannelPair(true) 168 + 169 + let initiatorClosed = false 170 + initiator.addEventListener('close', () => { 171 + initiatorClosed = true 172 + }) 173 + 174 + responder.destroy() 175 + 176 + // Wait for close to propagate 177 + await new Promise((resolve) => setTimeout(resolve, 10)) 178 + 179 + expect(initiator.connected).toBe(false) 180 + expect(responder.connected).toBe(false) 181 + expect(initiatorClosed).toBe(true) 182 + }) 183 + 184 + it('queues messages until nextMessage is called', async () => { 185 + const [initiator, responder] = createChannelPair(true) 186 + 187 + // Send multiple messages before awaiting 188 + initiator.send('first') 189 + initiator.send('second') 190 + initiator.send('third') 191 + 192 + // Wait a tick for delivery 193 + await new Promise((resolve) => setTimeout(resolve, 0)) 194 + 195 + // Should receive in order 196 + expect(await responder.nextMessage()).toBe('first') 197 + expect(await responder.nextMessage()).toBe('second') 198 + expect(await responder.nextMessage()).toBe('third') 199 + }) 200 + 201 + it('does not deliver messages before connect', async () => { 202 + const [initiator, responder] = createChannelPair() 203 + 204 + const messages: (string | ArrayBuffer)[] = [] 205 + responder.addEventListener('data', (event) => { 206 + messages.push(event.detail) 207 + }) 208 + 209 + // Send before connecting - should be silently dropped 210 + initiator.send('dropped message') 211 + 212 + // Wait a tick 213 + await new Promise((resolve) => setTimeout(resolve, 10)) 214 + 215 + expect(messages).toEqual([]) 216 + 217 + // Now connect and send 218 + initiator.connect() 219 + await new Promise((resolve) => setTimeout(resolve, 10)) 220 + 221 + initiator.send('delivered message') 222 + await new Promise((resolve) => setTimeout(resolve, 10)) 223 + 224 + expect(messages).toEqual(['delivered message']) 225 + }) 226 + 227 + it('sends ArrayBuffer data', async () => { 228 + const [initiator, responder] = createChannelPair(true) 229 + 230 + const buffer = new ArrayBuffer(4) 231 + const view = new Uint8Array(buffer) 232 + view.set([1, 2, 3, 4]) 233 + 234 + initiator.send(buffer) 235 + const received = await responder.nextMessage() 236 + 237 + expect(received).toBeInstanceOf(ArrayBuffer) 238 + expect(new Uint8Array(received as ArrayBuffer)).toEqual(new Uint8Array([1, 2, 3, 4])) 239 + }) 240 + })
+363
src/spec/helpers-socket-pair.ts
··· 1 + import {EventEmitter} from 'events' 2 + import type {WebSocket} from 'isomorphic-ws' 3 + 4 + import { 5 + DataChannelEventMap, 6 + DataChannelSendable, 7 + PeerConnectionStats, 8 + RTCSignalData, 9 + } from '#lib/client/webrtc' 10 + import {TypedEventTarget} from '#lib/events' 11 + 12 + /** 13 + * A mock WebSocket that can be paired with another MockSocket. 14 + * Implements the WebSocket interface that drivers expect. 15 + */ 16 + export class MockSocket extends EventEmitter { 17 + readonly CONNECTING = 0 as const 18 + readonly OPEN = 1 as const 19 + readonly CLOSING = 2 as const 20 + readonly CLOSED = 3 as const 21 + 22 + readyState: number = 0 // OPEN 23 + #peer: MockSocket | null = null 24 + #messageQueue: string[] = [] 25 + #messageResolvers: Array<(msg: string) => void> = [] 26 + 27 + // WebSocket compatibility properties 28 + binaryType: 'nodebuffer' | 'arraybuffer' | 'fragments' = 'nodebuffer' 29 + bufferedAmount: number = 0 30 + extensions: string = '' 31 + isPaused: boolean = false 32 + protocol: string = '' 33 + url: string = 'mock://socket' 34 + 35 + constructor(open = true) { 36 + super() 37 + if (open) { 38 + queueMicrotask(() => { 39 + this.open() 40 + }) 41 + } 42 + } 43 + 44 + // Stub methods for WebSocket compatibility 45 + ping() {} 46 + pong() {} 47 + terminate() { 48 + this.close() 49 + } 50 + 51 + /** 52 + * Link this socket to a peer socket. 53 + * Messages sent on one will be received by the other. 54 + */ 55 + link(peer: MockSocket) { 56 + this.#peer = peer 57 + peer.#peer = this 58 + } 59 + 60 + send(data: string | Buffer) { 61 + // silently ignore sends on closed socket (like real WebSocket) 62 + if (this.readyState !== this.OPEN) { 63 + console.warn('socket send bailing, socket closed') 64 + return 65 + } 66 + 67 + // Deliver to peer asynchronously (like real WebSocket) 68 + const message = typeof data === 'string' ? data : data.toString() 69 + if (this.#peer) { 70 + queueMicrotask(() => { 71 + this.#peer?._receive(message) 72 + }) 73 + } 74 + } 75 + 76 + open() { 77 + if (this.readyState === this.OPEN) return 78 + 79 + this.readyState = this.CONNECTING 80 + queueMicrotask(() => { 81 + this.readyState = this.OPEN 82 + this.emit('open') 83 + }) 84 + } 85 + 86 + close(code?: number, reason?: string) { 87 + if (this.readyState === this.CLOSED) return 88 + 89 + this.readyState = this.CLOSING 90 + 91 + queueMicrotask(() => { 92 + this.readyState = this.CLOSED 93 + this.emit('close', {code: code ?? 1000, reason: reason ?? ''}) 94 + 95 + // Close peer too 96 + if (this.#peer && this.#peer.readyState !== this.#peer.CLOSED) { 97 + this.#peer.close(code, reason) 98 + } 99 + }) 100 + } 101 + 102 + /** 103 + * Called by the peer when it sends a message. 104 + */ 105 + _receive(data: string) { 106 + if (this.readyState !== this.OPEN) return 107 + 108 + // If someone is waiting for a message, resolve immediately 109 + if (this.#messageResolvers.length > 0) { 110 + const resolver = this.#messageResolvers.shift()! 111 + resolver(data) 112 + } else { 113 + this.#messageQueue.push(data) 114 + } 115 + 116 + // Also emit the event for drivers that use addEventListener 117 + this.emit('message', {data}) 118 + } 119 + 120 + /** 121 + * Wait for the next message from the peer. 122 + * Used by tests to await responses. 123 + */ 124 + nextMessage(): Promise<string> { 125 + if (this.#messageQueue.length > 0) { 126 + return Promise.resolve(this.#messageQueue.shift()!) 127 + } 128 + 129 + return new Promise((resolve) => { 130 + this.#messageResolvers.push(resolve) 131 + }) 132 + } 133 + 134 + // WebSocket-compatible event listener methods 135 + addEventListener(event: string, handler: (event: unknown) => void) { 136 + this.on(event, handler) 137 + } 138 + 139 + removeEventListener(event: string, handler: (event: unknown) => void) { 140 + this.off(event, handler) 141 + } 142 + } 143 + 144 + /** 145 + * Create a pair of linked mock sockets. 146 + * Messages sent on one are received by the other. 147 + * 148 + * @returns [client, server] - Use client in tests, pass server to drivers 149 + * 150 + * @example 151 + * ```ts 152 + * const [client, server] = createSocketPair() 153 + * 154 + * // Pass server to the driver 155 + * realm.attachSocket(identid, server) 156 + * const driverPromise = driveRealm(server, auth) 157 + * 158 + * // Send messages from client, await responses 159 + * client.send(JSON.stringify(request)) 160 + * const response = await client.nextMessage() 161 + * ``` 162 + */ 163 + export function createSocketPair(open = true): [MockSocket & WebSocket, MockSocket & WebSocket] { 164 + const client = new MockSocket(open) 165 + const server = new MockSocket(open) 166 + client.link(server) 167 + return [client, server] as [MockSocket & WebSocket, MockSocket & WebSocket] 168 + } 169 + 170 + /** 171 + * A mock DataChannelPeer that can be paired with another MockDataChannel. 172 + * Implements the DataChannelPeer interface for testing RealmPeer without WebRTC. 173 + */ 174 + export class MockDataChannel extends TypedEventTarget<DataChannelEventMap> { 175 + readonly initiator: boolean 176 + 177 + #peer: MockDataChannel | null = null 178 + #connected = false 179 + #destroyed = false 180 + #messageQueue: (string | ArrayBuffer)[] = [] 181 + #messageResolvers: Array<(msg: string | ArrayBuffer) => void> = [] 182 + 183 + constructor(initiator: boolean) { 184 + super() 185 + this.initiator = initiator 186 + } 187 + 188 + /** 189 + * Link this channel to a peer channel. 190 + * Messages sent on one will be received by the other. 191 + */ 192 + link(peer: MockDataChannel) { 193 + this.#peer = peer 194 + peer.#peer = this 195 + } 196 + 197 + get empty() { 198 + return this.#messageQueue.length === 0 199 + } 200 + 201 + /** 202 + * Simulate the connection being established. 203 + * Call this after linking to trigger 'connect' events on both sides. 204 + */ 205 + connect() { 206 + if (this.#connected || this.#destroyed) return 207 + 208 + this.#connected = true 209 + queueMicrotask(() => { 210 + this.dispatchCustomEvent('connect') 211 + }) 212 + 213 + if (this.#peer && !this.#peer.#connected) { 214 + this.#peer.connect() 215 + } 216 + } 217 + 218 + /** 219 + * Simulate a temporary disconnection (not destroy). 220 + * This simulates network issues where the channel disconnects but can reconnect. 221 + */ 222 + disconnect() { 223 + if (!this.#connected || this.#destroyed) return 224 + 225 + this.#connected = false 226 + queueMicrotask(() => { 227 + this.dispatchCustomEvent('close') 228 + }) 229 + 230 + if (this.#peer && this.#peer.#connected) { 231 + this.#peer.disconnect() 232 + } 233 + } 234 + 235 + connectionStats(): Promise<PeerConnectionStats | null> { 236 + return Promise.resolve(null) 237 + } 238 + 239 + send(data: DataChannelSendable): void { 240 + if (!this.#connected || this.#destroyed) return 241 + 242 + // Convert to string or ArrayBuffer 243 + let message: string | ArrayBuffer 244 + if (typeof data === 'string') { 245 + message = data 246 + } else if (data instanceof ArrayBuffer) { 247 + message = data 248 + } else if (data instanceof Blob) { 249 + // For simplicity, we don't support Blob in tests 250 + throw new Error('MockDataChannel does not support Blob') 251 + } else { 252 + // ArrayBufferView 253 + message = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) 254 + } 255 + 256 + // Deliver to peer asynchronously 257 + if (this.#peer) { 258 + queueMicrotask(() => { 259 + this.#peer?._receive(message) 260 + }) 261 + } 262 + } 263 + 264 + signal(data: RTCSignalData): void { 265 + // In mock, we auto-connect when both peers have signaled 266 + // For now, just emit the signal event so RealmConnection can forward it 267 + if (this.#peer) { 268 + queueMicrotask(() => { 269 + this.#peer?.dispatchCustomEvent('signal', data) 270 + }) 271 + } 272 + } 273 + 274 + destroy(): void { 275 + if (this.#destroyed) return 276 + 277 + this.#destroyed = true 278 + this.#connected = false 279 + 280 + queueMicrotask(() => { 281 + this.dispatchCustomEvent('close') 282 + }) 283 + 284 + // Close peer too 285 + if (this.#peer && !this.#peer.#destroyed) { 286 + this.#peer.destroy() 287 + } 288 + } 289 + 290 + /** 291 + * Called by the peer when it sends a message. 292 + */ 293 + _receive(data: string | ArrayBuffer) { 294 + if (!this.#connected || this.#destroyed) return 295 + 296 + // If someone is waiting for a message, resolve immediately 297 + if (this.#messageResolvers.length > 0) { 298 + const resolver = this.#messageResolvers.shift()! 299 + resolver(data) 300 + } else { 301 + this.#messageQueue.push(data) 302 + } 303 + 304 + // Also emit the event for RealmPeer 305 + this.dispatchCustomEvent('data', data) 306 + } 307 + 308 + /** 309 + * Wait for the next message from the peer. 310 + * Used by tests to await responses. 311 + */ 312 + nextMessage(): Promise<string | ArrayBuffer> { 313 + if (this.#messageQueue.length > 0) { 314 + return Promise.resolve(this.#messageQueue.shift()!) 315 + } 316 + 317 + return new Promise((resolve) => { 318 + this.#messageResolvers.push(resolve) 319 + }) 320 + } 321 + 322 + /** 323 + * Check if the channel is connected. 324 + */ 325 + get connected(): boolean { 326 + return this.#connected 327 + } 328 + } 329 + 330 + /** 331 + * Create a pair of linked mock data channels. 332 + * Messages sent on one are received by the other. 333 + * 334 + * @param autoConnect - If true, automatically connect both channels 335 + * @returns [initiator, responder] - Both channels linked together 336 + * 337 + * @example 338 + * ```ts 339 + * const [initiator, responder] = createChannelPair() 340 + * 341 + * // Pass to RealmPeer constructors 342 + * const peer1 = new RealmPeer(realm, actions, remoteid, true, initiator) 343 + * const peer2 = new RealmPeer(realm, actions, remoteid, false, responder) 344 + * 345 + * // Connect the channels 346 + * initiator.connect() 347 + * 348 + * // Send messages 349 + * peer1.send('hello') 350 + * const msg = await responder.nextMessage() 351 + * ``` 352 + */ 353 + export function createChannelPair(autoConnect = false): [MockDataChannel, MockDataChannel] { 354 + const initiator = new MockDataChannel(true) 355 + const responder = new MockDataChannel(false) 356 + initiator.link(responder) 357 + 358 + if (autoConnect) { 359 + initiator.connect() 360 + } 361 + 362 + return [initiator, responder] 363 + }
+134 -13
src/spec/helpers-socket.ts
··· 1 - import {afterEach, beforeEach} from '@jest/globals' 2 - import {WebSocket} from 'isomorphic-ws' 3 - import WS from 'jest-websocket-mock' 1 + import {afterEach, beforeEach} from 'vitest' 2 + import {WebSocket, WebSocketServer} from 'ws' 3 + 4 + export type MockWebSocketServer = { 5 + url: string 6 + wss: WebSocketServer 7 + clients: Set<WebSocket> 8 + messages: unknown[] 9 + messageResolvers: Array<(msg: unknown) => void> 10 + connected: Promise<void> 11 + _resolveConnected: () => void 12 + } 13 + 14 + /** 15 + * Creates a mock WebSocket server for testing. 16 + * Compatible API with jest-websocket-mock for easier migration. 17 + */ 18 + export function createMockServer(url = 'ws://localhost:1234'): MockWebSocketServer { 19 + const parsedUrl = new URL(url) 20 + const port = parseInt(parsedUrl.port || '1234', 10) 21 + 22 + let resolveConnected: () => void 23 + const connected = new Promise<void>((resolve) => { 24 + resolveConnected = resolve 25 + }) 26 + 27 + const wss = new WebSocketServer({port}) 28 + 29 + const server: MockWebSocketServer = { 30 + url, 31 + wss, 32 + clients: new Set(), 33 + messages: [], 34 + messageResolvers: [], 35 + connected, 36 + _resolveConnected: resolveConnected!, 37 + } 38 + 39 + wss.on('connection', (ws) => { 40 + server.clients.add(ws) 41 + server._resolveConnected() 42 + 43 + // Reset connected promise for next connection 44 + server.connected = new Promise<void>((resolve) => { 45 + server._resolveConnected = resolve 46 + }) 47 + 48 + ws.on('message', (data) => { 49 + const message = data.toString() 50 + if (server.messageResolvers.length > 0) { 51 + const resolver = server.messageResolvers.shift()! 52 + resolver(message) 53 + } else { 54 + server.messages.push(message) 55 + } 56 + }) 57 + 58 + ws.on('close', () => { 59 + server.clients.delete(ws) 60 + }) 61 + }) 62 + 63 + return server 64 + } 65 + 66 + export const MockServer = { 67 + /** 68 + * Send a message to all connected clients 69 + */ 70 + send(server: MockWebSocketServer, data: string | object) { 71 + const message = typeof data === 'string' ? data : JSON.stringify(data) 72 + for (const client of server.clients) { 73 + client.send(message) 74 + } 75 + }, 76 + 77 + /** 78 + * Get the next message from any client 79 + */ 80 + nextMessage(server: MockWebSocketServer): Promise<unknown> { 81 + if (server.messages.length > 0) { 82 + return Promise.resolve(server.messages.shift()) 83 + } 84 + return new Promise((resolve) => { 85 + server.messageResolvers.push(resolve) 86 + }) 87 + }, 88 + 89 + /** 90 + * Close all client connections 91 + */ 92 + close(server: MockWebSocketServer) { 93 + for (const client of server.clients) { 94 + client.close() 95 + } 96 + }, 97 + 98 + /** 99 + * Trigger an error on all client connections by closing with abnormal code 100 + */ 101 + error(server: MockWebSocketServer) { 102 + for (const client of server.clients) { 103 + // Close with code 1011 (unexpected condition) to trigger error on client 104 + client.close(1011, 'Mock error') 105 + } 106 + }, 107 + 108 + /** 109 + * Clean up the server 110 + */ 111 + clean(server: MockWebSocketServer) { 112 + for (const client of server.clients) { 113 + client.close() 114 + } 115 + server.clients.clear() 116 + server.wss.close() 117 + }, 118 + } 4 119 5 120 export type SocketServerState = { 6 121 url: string 7 - server?: WS 122 + server?: MockWebSocketServer 8 123 } 9 124 125 + /** 126 + * Helper that sets up beforeEach/afterEach hooks for a mock WebSocket server. 127 + * This is the main helper for driver tests. 128 + */ 10 129 export function mockSocketServer(url = 'ws://localhost:1234') { 11 130 const state: SocketServerState = {url} 12 131 13 132 beforeEach(() => { 14 - state.server = new WS(url) 133 + state.server = createMockServer(url) 15 134 }) 16 135 17 136 afterEach(() => { 18 - WS.clean() 137 + if (state.server) { 138 + MockServer.clean(state.server) 139 + } 19 140 }) 20 141 21 142 return { 22 143 url, 23 144 state, 24 145 25 - get nextMessage() { 26 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 27 - return this.state.server!.nextMessage 146 + get nextMessage(): Promise<unknown> { 147 + return MockServer.nextMessage(state.server!) 28 148 }, 29 149 30 150 send(data: string | object) { 31 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 32 - this.state.server!.send(data) 151 + MockServer.send(state.server!, data) 33 152 }, 34 153 35 154 async connect(): Promise<WebSocket> { 36 155 const {promise, resolve, reject} = Promise.withResolvers<WebSocket>() 37 - const client = new WebSocket(this.state.url) 156 + // Use isomorphic-ws WebSocket for client (maintains compatibility with existing tests) 157 + const {WebSocket: IsomorphicWebSocket} = await import('isomorphic-ws') 158 + const client = new IsomorphicWebSocket(state.url) 38 159 39 160 const open = () => { 40 - resolve(client) 161 + resolve(client as unknown as WebSocket) 41 162 client.removeEventListener('open', open) 42 163 client.removeEventListener('error', error) 43 164 }
+8 -1
tsconfig.json
··· 45 45 "lib": ["es2024", "DOM", "DOM.Iterable", "DOM.AsyncIterable"] 46 46 }, 47 47 48 - "include": ["src/**/*", "node_modules/vite/client.d.ts", "vite.config.js", "eslint.config.js"], 48 + "include": [ 49 + "src/**/*", 50 + "node_modules/vite/client.d.ts", 51 + "vite.config.js", 52 + "vitest.config.ts", 53 + "vitest.setup.ts", 54 + "eslint.config.js" 55 + ], 49 56 "exclude": ["node_modules", "dist", "docs", "tmp"] 50 57 }
+9 -1
vite.config.js
··· 1 1 import tailwind from '@tailwindcss/vite' 2 + import basicSsl from '@vitejs/plugin-basic-ssl' 2 3 import devtools from 'solid-devtools/vite' 3 4 import {defineConfig} from 'vite' 4 5 import {analyzer} from 'vite-bundle-analyzer' ··· 18 19 define: { 19 20 global: {}, 20 21 }, 21 - plugins: [devtools(), analyzer({analyzerMode: 'static'}), solidPlugin(), tailwind()], 22 + plugins: [ 23 + devtools(), 24 + analyzer({analyzerMode: 'static'}), 25 + solidPlugin(), 26 + tailwind(), 27 + basicSsl({name: 'dev'}), 28 + ], 22 29 23 30 clearScreen: false, 24 31 server: { 32 + host: '0.0.0.0', 25 33 port: 4000, 26 34 proxy: { 27 35 '/api': 'http://127.0.0.1:4001',
+15
vitest.config.ts
··· 1 + import {defineConfig} from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['src/**/*.spec.{ts,tsx}'], 6 + environment: 'node', 7 + setupFiles: ['./vitest.setup.ts'], 8 + globals: false, 9 + coverage: { 10 + provider: 'v8', 11 + include: ['src/**/*.{ts,tsx}'], 12 + exclude: ['src/**/*.spec.{ts,tsx}', 'src/**/node_modules/**'], 13 + }, 14 + }, 15 + })
+1
vitest.setup.ts
··· 1 + import '@testing-library/jest-dom/vitest'