-38
jest.config.js
-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
-9
jest.setup.js
+410
-3840
package-lock.json
+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
+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
-1
src/lib/async/aborts.spec.ts
+1
-1
src/lib/async/blocking-atom.spec.ts
+1
-1
src/lib/async/blocking-atom.spec.ts
+1
-1
src/lib/async/blocking-queue.spec.ts
+1
-1
src/lib/async/blocking-queue.spec.ts
+1
-1
src/lib/async/semaphore.spec.ts
+1
-1
src/lib/async/semaphore.spec.ts
+1
-1
src/lib/async/sleep.spec.ts
+1
-1
src/lib/async/sleep.spec.ts
+15
-15
src/lib/breaker.spec.ts
+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
+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
-1
src/lib/crypto/cipher.spec.ts
+1
-1
src/lib/crypto/jwks.spec.ts
+1
-1
src/lib/crypto/jwks.spec.ts
+1
-1
src/lib/crypto/jwts.spec.ts
+1
-1
src/lib/crypto/jwts.spec.ts
+1
-1
src/lib/errors.spec.ts
+1
-1
src/lib/errors.spec.ts
+1
-1
src/lib/schema/brand.spec.ts
+1
-1
src/lib/schema/brand.spec.ts
+1
-1
src/lib/schema/json.spec.ts
+1
-1
src/lib/schema/json.spec.ts
+33
-26
src/lib/socket.spec.ts
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-1
src/realm/schema/timestamp.spec.ts
+2
-2
src/realm/schema/timestamp.ts
+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
+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
-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
-1
src/realm/server/realm.spec.ts
+1
-1
src/realm/server/realm.ts
+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
-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
+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
+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
+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
+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
+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
+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
vitest.setup.ts
···
1
+
import '@testing-library/jest-dom/vitest'