podcast manager

update eslint and do tsdoc and typescript lints

+440 -480
+51 -62
eslint.config.js
··· 1 1 import { includeIgnoreFile } from '@eslint/compat' 2 2 import js from '@eslint/js' 3 3 import json from '@eslint/json' 4 - import stylistic from '@stylistic/eslint-plugin' 5 4 import restrictedGlobals from 'confusing-browser-globals' 6 - import jsdoc from 'eslint-plugin-jsdoc' 5 + import tsdoc from 'eslint-plugin-tsdoc' 7 6 import react from 'eslint-plugin-react' 8 7 import reactHooks from 'eslint-plugin-react-hooks' 9 - import { defineConfig } from 'eslint/config' 10 8 import globals from 'globals' 9 + import tseslint from 'typescript-eslint' 11 10 import path from 'node:path' 12 - import { fileURLToPath } from 'node:url' 13 11 14 - const __filename = fileURLToPath(import.meta.url) 15 - const __dirname = path.dirname(__filename) 16 - const gitignore = path.resolve(__dirname, '.gitignore') 12 + const gitignore = path.resolve(import.meta.dirname, '.gitignore') 17 13 18 - export default defineConfig([ 14 + export default tseslint.config( 19 15 includeIgnoreFile(gitignore, '.gitignore'), 20 16 21 - // javascript 17 + // all files by default get shared globals 22 18 { 23 - files: ['**/*.{js,jsx}'], 24 - plugins: { js, jsdoc, stylistic }, 25 - extends: [ 26 - 'js/recommended', 27 - 'stylistic/recommended', 28 - 'jsdoc/flat/recommended-typescript-flavor', 29 - ], 19 + name: 'javascript basics', 20 + files: ['**/*.@(js|jsx|ts|tsx)'], 21 + 22 + plugins: {tsdoc}, 23 + extends: [js.configs.recommended], 24 + 30 25 languageOptions: { 31 - parserOptions: { 32 - emcaFeatures: { 33 - jsx: true, 34 - }, 35 - }, 36 26 globals: { 37 27 ...globals.es2024, 38 28 ...globals['shared-node-browser'], 39 29 }, 40 30 }, 31 + 41 32 rules: { 42 33 // eg, `open` is a global, but probably not really intended that way in our code 43 34 'no-restricted-globals': ['error', ...restrictedGlobals], 44 - 45 - // undef is handled by typecheck 46 - // for typechecking we want to allow unused vars like `import * as preact_types from 'preact'` 47 - 'no-undef': ['off'], 48 - 'no-unused-vars': ['warn', { varsIgnorePattern: '(?:^_)|(?:_types$)' }], 35 + 'no-unused-vars': ['warn', { varsIgnorePattern: '(?:^_)' }], 49 36 50 - // preferences 51 - 'max-len': ['warn', { code: 100 }], 52 - 'no-multi-spaces': ['off'], 53 - '@stylistic/dot-location': ['error', 'property'], 54 - '@stylistic/padded-blocks': ['warn', { classes: 'always', blocks: 'never' }], 37 + 'tsdoc/syntax': 'warn' 38 + }, 39 + }, 55 40 56 - // enforce docs 57 - 'jsdoc/check-indentation': ['warn'], 58 - 'jsdoc/tag-lines': ['warn', 'always', { count: 0, startLines: 1 }], 41 + { 42 + name: 'typescript basics', 43 + files: ['**/*.@(ts|tsx)'], 44 + extends: [tseslint.configs.strictTypeChecked], 45 + languageOptions: { 46 + parserOptions: { 47 + projectService: true, 48 + tsconfigRootDir: import.meta.dirname, 49 + }, 59 50 }, 51 + rules: { 52 + '@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '(?:^_)' }], 53 + '@typescript-eslint/no-unnecessary-condition': 'off', 54 + '@typescript-eslint/restrict-template-expressions': 'off', 55 + } 60 56 }, 61 57 62 - // server specific 63 58 { 64 - files: ['./src/server/*.js', './src/server/**/*.js', './src/cmd/*.js', './src/cmd/**/*.js'], 59 + name: 'node files', 60 + files: [ 61 + 'src/server/**/*.@(js|jsx|ts|tsx)', 62 + 'src/cmd/**/*.@(js|jsx|ts|tsx)' 63 + ], 65 64 languageOptions: { 66 65 globals: { 67 66 ...globals.es2024, ··· 70 69 }, 71 70 }, 72 71 73 - // client specific 74 - // mostly cribbed from preact's config, but that's not setup to handle eslint9 75 - // https://github.com/preactjs/eslint-config-preact/blob/master/index.js 76 72 { 77 - files: ['./src/client/*.{js,jsx}', './src/client/**/*.{js,jsx}'], 78 - plugins: { 79 - react, 80 - reactHooks, 81 - }, 73 + // mostly cribbed from preact's config, but that's not setup to handle eslint9 74 + // https://github.com/preactjs/eslint-config-preact/blob/master/index.js 75 + name: 'client files', 76 + files: [ 77 + 'src/client/**/*.@(js|jsx|ts|tsx)', 78 + ], 82 79 languageOptions: { 83 - parserOptions: { 84 - ecmaFeatures: { 85 - jsx: true, 86 - }, 87 - }, 88 80 globals: { 89 81 ...globals.es2024, 90 82 ...globals.browser, 91 83 }, 84 + }, 85 + plugins: { 86 + react, 87 + reactHooks, 92 88 }, 93 89 settings: { 94 90 react: { ··· 139 135 // tests don't have jsdoc requirements 140 136 { 141 137 files: ['src/**/*.spec.{js,jsx}'], 142 - rules: { 143 - // Disable JSDoc requirements for test files 144 - 'jsdoc/require-jsdoc': 'off', 145 - 'jsdoc/require-returns': 'off', 146 - 'jsdoc/require-param': 'off', 147 - 'jsdoc/require-param-description': 'off', 148 - 'jsdoc/require-param-type': 'off', 149 - 'jsdoc/check-tag-names': 'off', 150 - }, 138 + rules: {}, 151 139 }, 152 140 153 141 // json (with comments in some files) 154 142 { 155 143 files: ['**/*.json'], 156 144 ignores: ['package-lock.json'], 145 + 157 146 plugins: { json }, 158 147 language: 'json/json', 159 - extends: ['json/recommended'], 148 + extends: [ 149 + json.configs.recommended 150 + ], 160 151 }, 161 152 { 162 153 files: ['tsconfig.json'], 163 - plugins: { json }, 164 154 language: 'json/jsonc', 165 - extends: ['json/recommended'], 166 155 }, 167 - ]) 156 + )
-7
jest.config.js
··· 4 4 import gitignore from 'parse-gitignore' 5 5 import * as tsjest from 'ts-jest' 6 6 7 - import * as jest_types from 'jest' 8 - 9 - /** 10 - * @private 11 - * @returns {Array<RegExp>} list of path regexp to ignore 12 - */ 13 7 function gitignorePatterns() { 14 8 const __filename = fileURLToPath(import.meta.url) 15 9 const __dirname = path.dirname(__filename) ··· 20 14 .map(p => globToRegexp(p, { globstar: true }).source) 21 15 } 22 16 23 - /** @type {jest_types.Config} */ 24 17 export default { 25 18 testMatch: ['<rootDir>/src/**/*.spec.{ts,tsx}'], 26 19 testPathIgnorePatterns: [
+242 -254
package-lock.json
··· 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 11 "express": "^5.1.0", 12 + "isomorphic-ws": "^5.0.0", 12 13 "jose": "^6.0.11", 13 14 "nanoid": "^5.1.5", 14 15 "preact": "^10.26.9", ··· 26 27 "@faker-js/faker": "^9.8.0", 27 28 "@jest/globals": "^30.0.0", 28 29 "@preact/preset-vite": "^2.10.1", 29 - "@stylistic/eslint-plugin": "^4.4.1", 30 30 "@testing-library/jest-dom": "^6.6.3", 31 31 "@testing-library/preact": "^3.2.4", 32 32 "@types/confusing-browser-globals": "^1.0.3", ··· 37 37 "@types/ws": "^8.18.1", 38 38 "confusing-browser-globals": "^1.0.11", 39 39 "eslint": "^9.28.0", 40 - "eslint-plugin-jsdoc": "~51.0", 41 40 "eslint-plugin-react": "^7.37.5", 42 41 "eslint-plugin-react-hooks": "^5.2.0", 42 + "eslint-plugin-tsdoc": "^0.4.0", 43 43 "glob-to-regexp": "^0.4.1", 44 44 "globals": "^16.2.0", 45 45 "identity-obj-proxy": "^3.0.0", 46 46 "jest": "^30.0.0", 47 47 "jest-environment-jsdom": "^30.0.0", 48 48 "jest-fixed-jsdom": "^0.0.9", 49 - "jsdoc": "^4.0.4", 50 49 "jsdom": "^26.1.0", 51 50 "parse-gitignore": "^2.0.0", 52 51 "tidy-jsdoc-fork": "github:lygaret/tidy-jsdoc", 53 52 "ts-jest": "^29.4.0", 54 53 "typescript": "^5.8.3", 54 + "typescript-eslint": "^8.34.1", 55 55 "typescript-eslint-language-service": "^5.0.5", 56 56 "vite": "^6.3.5", 57 57 "vite-plugin-checker": "^0.9.3", ··· 805 805 "optional": true, 806 806 "dependencies": { 807 807 "tslib": "^2.4.0" 808 - } 809 - }, 810 - "node_modules/@es-joy/jsdoccomment": { 811 - "version": "0.50.2", 812 - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz", 813 - "integrity": "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==", 814 - "dev": true, 815 - "license": "MIT", 816 - "dependencies": { 817 - "@types/estree": "^1.0.6", 818 - "@typescript-eslint/types": "^8.11.0", 819 - "comment-parser": "1.4.1", 820 - "esquery": "^1.6.0", 821 - "jsdoc-type-pratt-parser": "~4.1.0" 822 - }, 823 - "engines": { 824 - "node": ">=18" 825 808 } 826 809 }, 827 810 "node_modules/@esbuild/aix-ppc64": { ··· 2212 2195 "@jridgewell/sourcemap-codec": "^1.4.14" 2213 2196 } 2214 2197 }, 2215 - "node_modules/@jsdoc/salty": { 2216 - "version": "0.2.9", 2217 - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", 2218 - "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", 2198 + "node_modules/@microsoft/tsdoc": { 2199 + "version": "0.15.1", 2200 + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", 2201 + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", 2202 + "dev": true, 2203 + "license": "MIT" 2204 + }, 2205 + "node_modules/@microsoft/tsdoc-config": { 2206 + "version": "0.17.1", 2207 + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", 2208 + "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", 2209 + "dev": true, 2210 + "license": "MIT", 2211 + "dependencies": { 2212 + "@microsoft/tsdoc": "0.15.1", 2213 + "ajv": "~8.12.0", 2214 + "jju": "~1.4.0", 2215 + "resolve": "~1.22.2" 2216 + } 2217 + }, 2218 + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { 2219 + "version": "8.12.0", 2220 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 2221 + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 2219 2222 "dev": true, 2220 - "license": "Apache-2.0", 2223 + "license": "MIT", 2221 2224 "dependencies": { 2222 - "lodash": "^4.17.21" 2225 + "fast-deep-equal": "^3.1.1", 2226 + "json-schema-traverse": "^1.0.0", 2227 + "require-from-string": "^2.0.2", 2228 + "uri-js": "^4.2.2" 2229 + }, 2230 + "funding": { 2231 + "type": "github", 2232 + "url": "https://github.com/sponsors/epoberezkin" 2233 + } 2234 + }, 2235 + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { 2236 + "version": "1.0.0", 2237 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 2238 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 2239 + "dev": true, 2240 + "license": "MIT" 2241 + }, 2242 + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { 2243 + "version": "1.22.10", 2244 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", 2245 + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", 2246 + "dev": true, 2247 + "license": "MIT", 2248 + "dependencies": { 2249 + "is-core-module": "^2.16.0", 2250 + "path-parse": "^1.0.7", 2251 + "supports-preserve-symlinks-flag": "^1.0.0" 2252 + }, 2253 + "bin": { 2254 + "resolve": "bin/resolve" 2223 2255 }, 2224 2256 "engines": { 2225 - "node": ">=v12.0.0" 2257 + "node": ">= 0.4" 2258 + }, 2259 + "funding": { 2260 + "url": "https://github.com/sponsors/ljharb" 2226 2261 } 2227 2262 }, 2228 2263 "node_modules/@napi-rs/wasm-runtime": { ··· 2761 2796 "@sinonjs/commons": "^3.0.1" 2762 2797 } 2763 2798 }, 2764 - "node_modules/@stylistic/eslint-plugin": { 2765 - "version": "4.4.1", 2766 - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", 2767 - "integrity": "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ==", 2768 - "dev": true, 2769 - "license": "MIT", 2770 - "dependencies": { 2771 - "@typescript-eslint/utils": "^8.32.1", 2772 - "eslint-visitor-keys": "^4.2.0", 2773 - "espree": "^10.3.0", 2774 - "estraverse": "^5.3.0", 2775 - "picomatch": "^4.0.2" 2776 - }, 2777 - "engines": { 2778 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2779 - }, 2780 - "peerDependencies": { 2781 - "eslint": ">=9.0.0" 2782 - } 2783 - }, 2784 - "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { 2785 - "version": "4.0.2", 2786 - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 2787 - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 2788 - "dev": true, 2789 - "license": "MIT", 2790 - "engines": { 2791 - "node": ">=12" 2792 - }, 2793 - "funding": { 2794 - "url": "https://github.com/sponsors/jonschlinkert" 2795 - } 2796 - }, 2797 2799 "node_modules/@testing-library/dom": { 2798 2800 "version": "8.20.1", 2799 2801 "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", ··· 3361 3363 "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", 3362 3364 "dev": true, 3363 3365 "license": "MIT", 3366 + "peer": true, 3364 3367 "dependencies": { 3365 3368 "@types/linkify-it": "^5", 3366 3369 "@types/mdurl": "^2" ··· 3502 3505 "dev": true, 3503 3506 "license": "MIT" 3504 3507 }, 3508 + "node_modules/@typescript-eslint/eslint-plugin": { 3509 + "version": "8.34.1", 3510 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", 3511 + "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", 3512 + "dev": true, 3513 + "license": "MIT", 3514 + "dependencies": { 3515 + "@eslint-community/regexpp": "^4.10.0", 3516 + "@typescript-eslint/scope-manager": "8.34.1", 3517 + "@typescript-eslint/type-utils": "8.34.1", 3518 + "@typescript-eslint/utils": "8.34.1", 3519 + "@typescript-eslint/visitor-keys": "8.34.1", 3520 + "graphemer": "^1.4.0", 3521 + "ignore": "^7.0.0", 3522 + "natural-compare": "^1.4.0", 3523 + "ts-api-utils": "^2.1.0" 3524 + }, 3525 + "engines": { 3526 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 3527 + }, 3528 + "funding": { 3529 + "type": "opencollective", 3530 + "url": "https://opencollective.com/typescript-eslint" 3531 + }, 3532 + "peerDependencies": { 3533 + "@typescript-eslint/parser": "^8.34.1", 3534 + "eslint": "^8.57.0 || ^9.0.0", 3535 + "typescript": ">=4.8.4 <5.9.0" 3536 + } 3537 + }, 3538 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { 3539 + "version": "7.0.5", 3540 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 3541 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 3542 + "dev": true, 3543 + "license": "MIT", 3544 + "engines": { 3545 + "node": ">= 4" 3546 + } 3547 + }, 3505 3548 "node_modules/@typescript-eslint/parser": { 3506 - "version": "8.34.0", 3507 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", 3508 - "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", 3549 + "version": "8.34.1", 3550 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", 3551 + "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", 3509 3552 "dev": true, 3510 3553 "license": "MIT", 3511 - "peer": true, 3512 3554 "dependencies": { 3513 - "@typescript-eslint/scope-manager": "8.34.0", 3514 - "@typescript-eslint/types": "8.34.0", 3515 - "@typescript-eslint/typescript-estree": "8.34.0", 3516 - "@typescript-eslint/visitor-keys": "8.34.0", 3555 + "@typescript-eslint/scope-manager": "8.34.1", 3556 + "@typescript-eslint/types": "8.34.1", 3557 + "@typescript-eslint/typescript-estree": "8.34.1", 3558 + "@typescript-eslint/visitor-keys": "8.34.1", 3517 3559 "debug": "^4.3.4" 3518 3560 }, 3519 3561 "engines": { ··· 3529 3571 } 3530 3572 }, 3531 3573 "node_modules/@typescript-eslint/project-service": { 3532 - "version": "8.34.0", 3533 - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", 3534 - "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", 3574 + "version": "8.34.1", 3575 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", 3576 + "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", 3535 3577 "dev": true, 3536 3578 "license": "MIT", 3537 3579 "dependencies": { 3538 - "@typescript-eslint/tsconfig-utils": "^8.34.0", 3539 - "@typescript-eslint/types": "^8.34.0", 3580 + "@typescript-eslint/tsconfig-utils": "^8.34.1", 3581 + "@typescript-eslint/types": "^8.34.1", 3540 3582 "debug": "^4.3.4" 3541 3583 }, 3542 3584 "engines": { ··· 3551 3593 } 3552 3594 }, 3553 3595 "node_modules/@typescript-eslint/scope-manager": { 3554 - "version": "8.34.0", 3555 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", 3556 - "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", 3596 + "version": "8.34.1", 3597 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", 3598 + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", 3557 3599 "dev": true, 3558 3600 "license": "MIT", 3559 3601 "dependencies": { 3560 - "@typescript-eslint/types": "8.34.0", 3561 - "@typescript-eslint/visitor-keys": "8.34.0" 3602 + "@typescript-eslint/types": "8.34.1", 3603 + "@typescript-eslint/visitor-keys": "8.34.1" 3562 3604 }, 3563 3605 "engines": { 3564 3606 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 3569 3611 } 3570 3612 }, 3571 3613 "node_modules/@typescript-eslint/tsconfig-utils": { 3572 - "version": "8.34.0", 3573 - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", 3574 - "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", 3614 + "version": "8.34.1", 3615 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", 3616 + "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", 3575 3617 "dev": true, 3576 3618 "license": "MIT", 3577 3619 "engines": { ··· 3585 3627 "typescript": ">=4.8.4 <5.9.0" 3586 3628 } 3587 3629 }, 3630 + "node_modules/@typescript-eslint/type-utils": { 3631 + "version": "8.34.1", 3632 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", 3633 + "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", 3634 + "dev": true, 3635 + "license": "MIT", 3636 + "dependencies": { 3637 + "@typescript-eslint/typescript-estree": "8.34.1", 3638 + "@typescript-eslint/utils": "8.34.1", 3639 + "debug": "^4.3.4", 3640 + "ts-api-utils": "^2.1.0" 3641 + }, 3642 + "engines": { 3643 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 3644 + }, 3645 + "funding": { 3646 + "type": "opencollective", 3647 + "url": "https://opencollective.com/typescript-eslint" 3648 + }, 3649 + "peerDependencies": { 3650 + "eslint": "^8.57.0 || ^9.0.0", 3651 + "typescript": ">=4.8.4 <5.9.0" 3652 + } 3653 + }, 3588 3654 "node_modules/@typescript-eslint/types": { 3589 - "version": "8.34.0", 3590 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", 3591 - "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", 3655 + "version": "8.34.1", 3656 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", 3657 + "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", 3592 3658 "dev": true, 3593 3659 "license": "MIT", 3594 3660 "engines": { ··· 3600 3666 } 3601 3667 }, 3602 3668 "node_modules/@typescript-eslint/typescript-estree": { 3603 - "version": "8.34.0", 3604 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", 3605 - "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", 3669 + "version": "8.34.1", 3670 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", 3671 + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", 3606 3672 "dev": true, 3607 3673 "license": "MIT", 3608 3674 "dependencies": { 3609 - "@typescript-eslint/project-service": "8.34.0", 3610 - "@typescript-eslint/tsconfig-utils": "8.34.0", 3611 - "@typescript-eslint/types": "8.34.0", 3612 - "@typescript-eslint/visitor-keys": "8.34.0", 3675 + "@typescript-eslint/project-service": "8.34.1", 3676 + "@typescript-eslint/tsconfig-utils": "8.34.1", 3677 + "@typescript-eslint/types": "8.34.1", 3678 + "@typescript-eslint/visitor-keys": "8.34.1", 3613 3679 "debug": "^4.3.4", 3614 3680 "fast-glob": "^3.3.2", 3615 3681 "is-glob": "^4.0.3", ··· 3642 3708 } 3643 3709 }, 3644 3710 "node_modules/@typescript-eslint/utils": { 3645 - "version": "8.34.0", 3646 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", 3647 - "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", 3711 + "version": "8.34.1", 3712 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", 3713 + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", 3648 3714 "dev": true, 3649 3715 "license": "MIT", 3650 3716 "dependencies": { 3651 3717 "@eslint-community/eslint-utils": "^4.7.0", 3652 - "@typescript-eslint/scope-manager": "8.34.0", 3653 - "@typescript-eslint/types": "8.34.0", 3654 - "@typescript-eslint/typescript-estree": "8.34.0" 3718 + "@typescript-eslint/scope-manager": "8.34.1", 3719 + "@typescript-eslint/types": "8.34.1", 3720 + "@typescript-eslint/typescript-estree": "8.34.1" 3655 3721 }, 3656 3722 "engines": { 3657 3723 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 3666 3732 } 3667 3733 }, 3668 3734 "node_modules/@typescript-eslint/visitor-keys": { 3669 - "version": "8.34.0", 3670 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", 3671 - "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", 3735 + "version": "8.34.1", 3736 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", 3737 + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", 3672 3738 "dev": true, 3673 3739 "license": "MIT", 3674 3740 "dependencies": { 3675 - "@typescript-eslint/types": "8.34.0", 3676 - "eslint-visitor-keys": "^4.2.0" 3741 + "@typescript-eslint/types": "8.34.1", 3742 + "eslint-visitor-keys": "^4.2.1" 3677 3743 }, 3678 3744 "engines": { 3679 3745 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 4126 4192 "node": ">= 8" 4127 4193 } 4128 4194 }, 4129 - "node_modules/are-docs-informative": { 4130 - "version": "0.1.1", 4131 - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.1.1.tgz", 4132 - "integrity": "sha512-sqRsNQBwbKLRX0jV5Cu5uzmtflf892n4Vukz7T659ebL4pz3mpOqCMU7lxMoBTFwnp10E3YB5ZcyHM41W5bcDA==", 4133 - "dev": true, 4134 - "license": "MIT", 4135 - "engines": { 4136 - "node": ">=18" 4137 - } 4138 - }, 4139 4195 "node_modules/argparse": { 4140 4196 "version": "2.0.1", 4141 4197 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", ··· 5674 5730 "dev": true, 5675 5731 "license": "MIT" 5676 5732 }, 5677 - "node_modules/comment-parser": { 5678 - "version": "1.4.1", 5679 - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", 5680 - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", 5681 - "dev": true, 5682 - "license": "MIT", 5683 - "engines": { 5684 - "node": ">= 12.0.0" 5685 - } 5686 - }, 5687 5733 "node_modules/compact2string": { 5688 5734 "version": "1.4.1", 5689 5735 "resolved": "https://registry.npmjs.org/compact2string/-/compact2string-1.4.1.tgz", ··· 7036 7082 } 7037 7083 } 7038 7084 }, 7039 - "node_modules/eslint-plugin-jsdoc": { 7040 - "version": "51.0.1", 7041 - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.0.1.tgz", 7042 - "integrity": "sha512-nnH6O8uk0Wp5EvHlVEPESKdGWTlu5g1tfBUZmL/jMZLBpUtttxxW+9hPzTMCYmYsQ3HwDsJdHJAiaDRKsP6iUg==", 7043 - "dev": true, 7044 - "license": "BSD-3-Clause", 7045 - "dependencies": { 7046 - "@es-joy/jsdoccomment": "~0.50.2", 7047 - "are-docs-informative": "^0.1.1", 7048 - "comment-parser": "1.4.1", 7049 - "debug": "^4.4.1", 7050 - "escape-string-regexp": "^4.0.0", 7051 - "espree": "^10.3.0", 7052 - "esquery": "^1.6.0", 7053 - "parse-imports-exports": "^0.2.4", 7054 - "semver": "^7.7.2", 7055 - "spdx-expression-parse": "^4.0.0" 7056 - }, 7057 - "engines": { 7058 - "node": ">=22" 7059 - }, 7060 - "peerDependencies": { 7061 - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" 7062 - } 7063 - }, 7064 - "node_modules/eslint-plugin-jsdoc/node_modules/semver": { 7065 - "version": "7.7.2", 7066 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 7067 - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 7068 - "dev": true, 7069 - "license": "ISC", 7070 - "bin": { 7071 - "semver": "bin/semver.js" 7072 - }, 7073 - "engines": { 7074 - "node": ">=10" 7075 - } 7076 - }, 7077 7085 "node_modules/eslint-plugin-react": { 7078 7086 "version": "7.37.5", 7079 7087 "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", ··· 7142 7150 }, 7143 7151 "engines": { 7144 7152 "node": "*" 7153 + } 7154 + }, 7155 + "node_modules/eslint-plugin-tsdoc": { 7156 + "version": "0.4.0", 7157 + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.4.0.tgz", 7158 + "integrity": "sha512-MT/8b4aKLdDClnS8mP3R/JNjg29i0Oyqd/0ym6NnQf+gfKbJJ4ZcSh2Bs1H0YiUMTBwww5JwXGTWot/RwyJ7aQ==", 7159 + "dev": true, 7160 + "license": "MIT", 7161 + "dependencies": { 7162 + "@microsoft/tsdoc": "0.15.1", 7163 + "@microsoft/tsdoc-config": "0.17.1" 7145 7164 } 7146 7165 }, 7147 7166 "node_modules/eslint-scope": { ··· 8102 8121 "dev": true, 8103 8122 "license": "ISC" 8104 8123 }, 8124 + "node_modules/graphemer": { 8125 + "version": "1.4.0", 8126 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 8127 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 8128 + "dev": true, 8129 + "license": "MIT" 8130 + }, 8105 8131 "node_modules/harmony-reflect": { 8106 8132 "version": "1.6.2", 8107 8133 "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", ··· 9090 9116 "node": ">=10" 9091 9117 } 9092 9118 }, 9119 + "node_modules/isomorphic-ws": { 9120 + "version": "5.0.0", 9121 + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", 9122 + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", 9123 + "license": "MIT", 9124 + "peerDependencies": { 9125 + "ws": "*" 9126 + } 9127 + }, 9093 9128 "node_modules/istanbul-lib-coverage": { 9094 9129 "version": "3.2.2", 9095 9130 "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", ··· 9905 9940 "url": "https://github.com/chalk/supports-color?sponsor=1" 9906 9941 } 9907 9942 }, 9943 + "node_modules/jju": { 9944 + "version": "1.4.0", 9945 + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", 9946 + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", 9947 + "dev": true, 9948 + "license": "MIT" 9949 + }, 9908 9950 "node_modules/join-async-iterator": { 9909 9951 "version": "1.1.1", 9910 9952 "resolved": "https://registry.npmjs.org/join-async-iterator/-/join-async-iterator-1.1.1.tgz", ··· 9958 10000 "dev": true, 9959 10001 "license": "MIT" 9960 10002 }, 9961 - "node_modules/jsdoc": { 9962 - "version": "4.0.4", 9963 - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", 9964 - "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", 9965 - "dev": true, 9966 - "license": "Apache-2.0", 9967 - "dependencies": { 9968 - "@babel/parser": "^7.20.15", 9969 - "@jsdoc/salty": "^0.2.1", 9970 - "@types/markdown-it": "^14.1.1", 9971 - "bluebird": "^3.7.2", 9972 - "catharsis": "^0.9.0", 9973 - "escape-string-regexp": "^2.0.0", 9974 - "js2xmlparser": "^4.0.2", 9975 - "klaw": "^3.0.0", 9976 - "markdown-it": "^14.1.0", 9977 - "markdown-it-anchor": "^8.6.7", 9978 - "marked": "^4.0.10", 9979 - "mkdirp": "^1.0.4", 9980 - "requizzle": "^0.2.3", 9981 - "strip-json-comments": "^3.1.0", 9982 - "underscore": "~1.13.2" 9983 - }, 9984 - "bin": { 9985 - "jsdoc": "jsdoc.js" 9986 - }, 9987 - "engines": { 9988 - "node": ">=12.0.0" 9989 - } 9990 - }, 9991 - "node_modules/jsdoc-type-pratt-parser": { 9992 - "version": "4.1.0", 9993 - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", 9994 - "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", 9995 - "dev": true, 9996 - "license": "MIT", 9997 - "engines": { 9998 - "node": ">=12.0.0" 9999 - } 10000 - }, 10001 - "node_modules/jsdoc/node_modules/escape-string-regexp": { 10002 - "version": "2.0.0", 10003 - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", 10004 - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", 10005 - "dev": true, 10006 - "license": "MIT", 10007 - "engines": { 10008 - "node": ">=8" 10009 - } 10010 - }, 10011 10003 "node_modules/jsdom": { 10012 10004 "version": "26.1.0", 10013 10005 "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", ··· 10257 10249 "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", 10258 10250 "dev": true, 10259 10251 "license": "MIT", 10252 + "peer": true, 10260 10253 "dependencies": { 10261 10254 "uc.micro": "^2.0.0" 10262 10255 } ··· 10506 10499 "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", 10507 10500 "dev": true, 10508 10501 "license": "MIT", 10502 + "peer": true, 10509 10503 "dependencies": { 10510 10504 "argparse": "^2.0.1", 10511 10505 "entities": "^4.4.0", ··· 10831 10825 "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", 10832 10826 "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", 10833 10827 "dev": true, 10834 - "license": "MIT" 10828 + "license": "MIT", 10829 + "peer": true 10835 10830 }, 10836 10831 "node_modules/media-typer": { 10837 10832 "version": "1.1.0", ··· 12298 12293 "node": ">=14" 12299 12294 } 12300 12295 }, 12301 - "node_modules/parse-imports-exports": { 12302 - "version": "0.2.4", 12303 - "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", 12304 - "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", 12305 - "dev": true, 12306 - "license": "MIT", 12307 - "dependencies": { 12308 - "parse-statements": "1.0.11" 12309 - } 12310 - }, 12311 12296 "node_modules/parse-json": { 12312 12297 "version": "5.2.0", 12313 12298 "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", ··· 12326 12311 "funding": { 12327 12312 "url": "https://github.com/sponsors/sindresorhus" 12328 12313 } 12329 - }, 12330 - "node_modules/parse-statements": { 12331 - "version": "1.0.11", 12332 - "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", 12333 - "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", 12334 - "dev": true, 12335 - "license": "MIT" 12336 12314 }, 12337 12315 "node_modules/parse-torrent": { 12338 12316 "version": "11.0.18", ··· 12854 12832 "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", 12855 12833 "dev": true, 12856 12834 "license": "MIT", 12835 + "peer": true, 12857 12836 "engines": { 12858 12837 "node": ">=6" 12859 12838 } ··· 13175 13154 "version": "2.1.1", 13176 13155 "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 13177 13156 "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 13157 + "dev": true, 13158 + "license": "MIT", 13159 + "engines": { 13160 + "node": ">=0.10.0" 13161 + } 13162 + }, 13163 + "node_modules/require-from-string": { 13164 + "version": "2.0.2", 13165 + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 13166 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 13178 13167 "dev": true, 13179 13168 "license": "MIT", 13180 13169 "engines": { ··· 13940 13929 "buffer-from": "^1.0.0", 13941 13930 "source-map": "^0.6.0" 13942 13931 } 13943 - }, 13944 - "node_modules/spdx-exceptions": { 13945 - "version": "2.5.0", 13946 - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", 13947 - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", 13948 - "dev": true, 13949 - "license": "CC-BY-3.0" 13950 - }, 13951 - "node_modules/spdx-expression-parse": { 13952 - "version": "4.0.0", 13953 - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", 13954 - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", 13955 - "dev": true, 13956 - "license": "MIT", 13957 - "dependencies": { 13958 - "spdx-exceptions": "^2.1.0", 13959 - "spdx-license-ids": "^3.0.0" 13960 - } 13961 - }, 13962 - "node_modules/spdx-license-ids": { 13963 - "version": "3.0.21", 13964 - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", 13965 - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", 13966 - "dev": true, 13967 - "license": "CC0-1.0" 13968 13932 }, 13969 13933 "node_modules/speed-limiter": { 13970 13934 "version": "1.0.2", ··· 15193 15157 "node": ">=14.17" 15194 15158 } 15195 15159 }, 15160 + "node_modules/typescript-eslint": { 15161 + "version": "8.34.1", 15162 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", 15163 + "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", 15164 + "dev": true, 15165 + "license": "MIT", 15166 + "dependencies": { 15167 + "@typescript-eslint/eslint-plugin": "8.34.1", 15168 + "@typescript-eslint/parser": "8.34.1", 15169 + "@typescript-eslint/utils": "8.34.1" 15170 + }, 15171 + "engines": { 15172 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 15173 + }, 15174 + "funding": { 15175 + "type": "opencollective", 15176 + "url": "https://opencollective.com/typescript-eslint" 15177 + }, 15178 + "peerDependencies": { 15179 + "eslint": "^8.57.0 || ^9.0.0", 15180 + "typescript": ">=4.8.4 <5.9.0" 15181 + } 15182 + }, 15196 15183 "node_modules/typescript-eslint-language-service": { 15197 15184 "version": "5.0.5", 15198 15185 "resolved": "https://registry.npmjs.org/typescript-eslint-language-service/-/typescript-eslint-language-service-5.0.5.tgz", ··· 15210 15197 "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", 15211 15198 "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", 15212 15199 "dev": true, 15213 - "license": "MIT" 15200 + "license": "MIT", 15201 + "peer": true 15214 15202 }, 15215 15203 "node_modules/uint8-util": { 15216 15204 "version": "2.2.5",
+3 -3
package.json
··· 21 21 }, 22 22 "dependencies": { 23 23 "express": "^5.1.0", 24 + "isomorphic-ws": "^5.0.0", 24 25 "jose": "^6.0.11", 25 26 "nanoid": "^5.1.5", 26 27 "preact": "^10.26.9", ··· 38 39 "@faker-js/faker": "^9.8.0", 39 40 "@jest/globals": "^30.0.0", 40 41 "@preact/preset-vite": "^2.10.1", 41 - "@stylistic/eslint-plugin": "^4.4.1", 42 42 "@testing-library/jest-dom": "^6.6.3", 43 43 "@testing-library/preact": "^3.2.4", 44 44 "@types/confusing-browser-globals": "^1.0.3", ··· 49 49 "@types/ws": "^8.18.1", 50 50 "confusing-browser-globals": "^1.0.11", 51 51 "eslint": "^9.28.0", 52 - "eslint-plugin-jsdoc": "~51.0", 53 52 "eslint-plugin-react": "^7.37.5", 54 53 "eslint-plugin-react-hooks": "^5.2.0", 54 + "eslint-plugin-tsdoc": "^0.4.0", 55 55 "glob-to-regexp": "^0.4.1", 56 56 "globals": "^16.2.0", 57 57 "identity-obj-proxy": "^3.0.0", 58 58 "jest": "^30.0.0", 59 59 "jest-environment-jsdom": "^30.0.0", 60 60 "jest-fixed-jsdom": "^0.0.9", 61 - "jsdoc": "^4.0.4", 62 61 "jsdom": "^26.1.0", 63 62 "parse-gitignore": "^2.0.0", 64 63 "tidy-jsdoc-fork": "github:lygaret/tidy-jsdoc", 65 64 "ts-jest": "^29.4.0", 66 65 "typescript": "^5.8.3", 66 + "typescript-eslint": "^8.34.1", 67 67 "typescript-eslint-language-service": "^5.0.5", 68 68 "vite": "^6.3.5", 69 69 "vite-plugin-checker": "^0.9.3",
+4 -3
src/client/components/messenger.tsx
··· 1 1 import { RealmConnection } from '#client/realm/connection.js' 2 + import { IdentID } from '#common/protocol' 2 3 import { useState, useEffect, useCallback } from 'preact/hooks' 3 4 4 5 export type MessengerProps = { ··· 7 8 8 9 export const Messenger: preact.FunctionComponent<{ webrtcManager: RealmConnection }> = (props) => { 9 10 const { webrtcManager } = props 10 - const [messages, setMessages] = useState<any[]>([]) 11 + const [messages, setMessages] = useState<[IdentID, string][]>([]) 11 12 12 - const peerdata = useCallback((event: CustomEvent) => { 13 - setMessages([...messages, [event.detail.remoteId, event.detail.data]]) 13 + const peerdata = useCallback((event: CustomEvent<{ remoteId: IdentID, data: unknown }>) => { 14 + setMessages([...messages, [event.detail.remoteId, `${event.detail.data}`]]) 14 15 }, [messages]) 15 16 16 17 const sendMessage = useCallback(() => {
+19 -28
src/client/realm/connection.ts
··· 1 - /** @module client/realm */ 2 - 3 1 import { nanoid } from 'nanoid' 4 2 import SimplePeer from 'simple-peer' 3 + import WebSocket from 'isomorphic-ws' 4 + import { z } from 'zod/v4' 5 5 6 6 import { generateSignableJwt, jwkExport } from '#common/crypto/jwks' 7 7 import { normalizeError, normalizeProtocolError, ProtocolError } from '#common/errors' 8 - import { IdentID, RealmBroadcastMessage, realmFromServerMessageSchema, RealmID, RealmRtcPeerWelcomeMessage, realmRtcPeerWelcomeMessageSchema, RealmRtcSignalMessage } from '#common/protocol' 8 + import { IdentID, parseJson, PreauthRegisterMessage, RealmBroadcastMessage, realmFromServerMessageSchema, RealmID, RealmRtcPeerWelcomeMessage, realmRtcPeerWelcomeMessageSchema, RealmRtcSignalMessage } from '#common/protocol' 9 9 import { sendSocket, streamSocketJson, takeSocketJson } from '#common/socket' 10 10 11 11 /** the state of a specific peer */ ··· 66 66 67 67 const pubkey = await jwkExport.parseAsync(this.#identity.keypair.publicKey) 68 68 this.#socket.send( 69 - await this.#signJwt( 70 - /** @type {protocol_types.PreauthRegisterMessage} */ 71 - ({ msg: 'preauth.register', pubkey }), 72 - ), 69 + await this.#signJwt({ msg: 'preauth.register', pubkey } as PreauthRegisterMessage), 73 70 ) 74 71 75 72 // the next message should be a welcome message ··· 128 125 129 126 // may not have a connection yet if we're waiting for them to answer 130 127 if (peer) { 131 - peer.signal(JSON.parse(parse.data.payload)) 128 + peer.signal(parse.data.payload) 132 129 } 133 130 134 131 continue ··· 148 145 } 149 146 } 150 147 151 - #handleSocketError: WebSocket['onerror'] = async (exc) => { 148 + #handleSocketError: WebSocket['onerror'] = (exc) => { 152 149 this.#dispatchCustomEvent('wserror', { error: normalizeProtocolError(exc) }) 153 150 this.destroy() 154 151 } 155 152 156 - /** @type {WebSocket['onclose']} */ 157 - #handleSocketClose: WebSocket['onclose'] = async () => { 153 + #handleSocketClose: WebSocket['onclose'] = () => { 158 154 this.#dispatchCustomEvent('wsclose') 159 155 this.destroy() 160 156 } ··· 224 220 this.#dispatchCustomEvent('peererror', { remoteid, error: err }) 225 221 }) 226 222 227 - peer.on('message', (data) => { 223 + peer.on('message', (data: unknown) => { 228 224 this.#dispatchCustomEvent('peerdata', { remoteid, data }) 229 225 }) 230 226 ··· 274 270 sendSocket(this.#socket, resp) 275 271 } 276 272 277 - /** 278 - * @returns {Record<protocol_types.IdentID, PeerState>} the current peer state mapping 279 - */ 273 + /** @returns the current peer state mapping */ 280 274 getPeerStates(): Record<IdentID, PeerState> { 281 275 const states: Record<IdentID, PeerState> = {} 282 276 for (const [peerId, peer] of this.#peers) { ··· 292 286 293 287 } 294 288 289 + const peerPingSchema = z.object({ type: z.literal('ping'), timestamp: z.number() }) 290 + 295 291 /** 296 292 * a peer belonging to the connection manager 297 293 */ ··· 340 336 341 337 #handlePeerData = (chunk: string) => { 342 338 try { 343 - const parsed = JSON.parse(chunk) 344 - switch (parsed.type) { 345 - // there are some connection-manager internal messages 346 - case 'pong': 347 - this.send( 348 - JSON.stringify({ type: 'pong', timestamp: parsed.timestamp }), 349 - ) 350 - break 351 - 352 - // dispatch others messages 353 - default: 354 - this.emit('message', parsed) 355 - break 339 + const ping = parseJson.pipe(peerPingSchema).safeParse(chunk) 340 + if (ping.success) { 341 + this.send( 342 + JSON.stringify({ type: 'pong', timestamp: ping.data.timestamp }), 343 + ) 344 + } 345 + else { 346 + this.emit('message', chunk) 356 347 } 357 348 } 358 349 catch (err) {
+11 -5
src/client/webrtc-demo.tsx
··· 68 68 } 69 69 }) 70 70 71 - const connect = useCallback(async () => { 72 - const realmid = protocol.RealmBrand.parse('realm-n7-qM0rOzsJ8N-iF') // hard code for now 73 - const identid = protocol.IdentBrand.generate() 74 - const keypair = await generateSigningJwkPair() 71 + const connect = useCallback(() => { 72 + const go = async () => { 73 + const realmid = protocol.RealmBrand.parse('realm-n7-qM0rOzsJ8N-iF') // hard code for now 74 + const identid = protocol.IdentBrand.generate() 75 + const keypair = await generateSigningJwkPair() 75 76 76 - context.setIdentity({ realmid, identid, keypair }) 77 + context.setIdentity({ realmid, identid, keypair }) 78 + } 79 + 80 + go().catch((e: unknown) => { 81 + console.error('couldnt create identity', e) 82 + }) 77 83 }, [context]) 78 84 79 85 return (
+3 -3
src/common/async/aborts.ts
··· 24 24 } 25 25 26 26 /** 27 - * @param signals the list of signals to combine 27 + * @param signals - the list of signals to combine 28 28 * @returns a combined signal, which will abort when any given signal does 29 29 */ 30 30 export function combineSignals(...signals: Array<AbortSignal | undefined>): AbortSignal { ··· 46 46 } 47 47 48 48 signal.addEventListener('abort', handler) 49 - cleanups.push(() => signal.removeEventListener('abort', handler)) 49 + cleanups.push(() => { signal.removeEventListener('abort', handler); }) 50 50 } 51 51 52 52 controller.signal.addEventListener('abort', () => { 53 - cleanups.forEach(cb => cb()) 53 + cleanups.forEach(cb => { cb(); }) 54 54 }) 55 55 56 56 return controller.signal
+4 -6
src/common/async/blocking-atom.ts
··· 1 - /** @module common/async */ 2 - 3 1 import { Semaphore } from './semaphore.js' 4 2 5 3 /** 6 4 * simple blocking atom, for waiting for a value. 7 5 * cribbed mostly from {@link https://github.com/ComFreek/async-playground} 8 - * 9 - * @template T - the type we're holding 10 6 */ 11 7 export class BlockingAtom<T> { 12 8 ··· 28 24 * tries to get the item from the atom, and blocks until available. 29 25 * 30 26 * @example 31 - * if (await atom.take()) 32 - * console.log('got it!') 27 + * ``` 28 + * if (await atom.take()) 29 + * console.log('got it!') 30 + * ``` 33 31 * 34 32 * @param signal - an abort signal to cancel the await 35 33 * @returns a promise for the item, or undefined if something aborted.
+2 -4
src/common/async/blocking-queue.ts
··· 1 - /** @module common/async */ 2 - 3 1 import { Semaphore } from './semaphore.js' 4 2 5 3 /** ··· 18 16 this.#items = [] 19 17 } 20 18 21 - /** @returns {number} how deep is the queue? */ 19 + /** @returns the depth of the queue */ 22 20 get depth(): number { 23 21 return this.#items.length 24 22 } ··· 50 48 /** 51 49 * block while waiting for an item off the queue. 52 50 * 53 - * @param [signal] a signal to use for aborting the block. 51 + * @param signal - a signal to use for aborting the block. 54 52 * @returns the item off the queue; rejects if aborted. 55 53 */ 56 54 async dequeue(signal?: AbortSignal): Promise<T> {
+5 -6
src/common/async/semaphore.ts
··· 1 - /** @module common/async */ 2 - 3 1 /** 4 2 * Simple counting semaphore, for blocking async ops. 5 3 * cribbed mostly from {@link https://github.com/ComFreek/async-playground} ··· 17 15 * try to take from the semaphore, reducing it's count 18 16 * if the semaphore is empty, blocks until available, or the given signal aborts. 19 17 * 20 - * @param signal a signal to use to abort the block 18 + * @param signal - a signal to use to abort the block 21 19 * @returns true if the semaphore was successfully taken, false if aborted. 22 20 */ 23 21 take(signal?: AbortSignal): Promise<boolean> { 24 22 return new Promise((resolve) => { 25 - if (signal?.aborted) return resolve(false) 23 + if (signal?.aborted) { resolve(false); return; } 26 24 27 25 // if there's resources available, use them 28 26 29 27 this.#counter-- 30 - if (this.#counter >= 0) return resolve(true) 28 + if (this.#counter >= 0) { resolve(true); return; } 31 29 32 30 // otherwise add to pending 33 31 // and explicitly remove the resolver from the list on abort ··· 59 57 60 58 if (this.#resolvers.length > 0) { 61 59 const resolver = this.#resolvers.shift() 62 - resolver && queueMicrotask(() => resolver(true)) 60 + if (resolver) 61 + queueMicrotask(() => { resolver(true); }) 63 62 } 64 63 } 65 64
+13 -14
src/common/async/sleep.ts
··· 1 - /** @module common/async */ 2 - 3 1 /** 4 - * @param ms the number of ms to sleep 5 - * @param [signal] an aptional abort signal, to cancel the sleep 2 + * @param ms - the number of ms to sleep 3 + * @param signal - an aptional abort signal, to cancel the sleep 6 4 * @returns a promise that resolves after given amount of time, and is interruptable with an abort signal. 7 5 */ 8 6 export function sleep(ms: number, signal?: AbortSignal): Promise<void> { 9 7 signal?.throwIfAborted() 10 8 9 + // not sure why this error is coming up 10 + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 11 11 const { resolve, reject, promise } = Promise.withResolvers<void>() 12 12 const timeout = setTimeout(resolve, ms) 13 13 14 - if (signal) { 15 - const abortHandler = () => { 16 - clearTimeout(timeout) 17 - reject(signal.reason) 18 - } 14 + if (!signal) 15 + return promise 19 16 20 - signal.addEventListener('abort', abortHandler) 21 - promise.finally(() => { 22 - signal.removeEventListener('abort', abortHandler) 23 - }) 17 + const abortHandler = () => { 18 + clearTimeout(timeout) 19 + reject(signal.reason) 24 20 } 25 21 26 - return promise 22 + signal.addEventListener('abort', abortHandler) 23 + return promise.finally(() => { 24 + signal.removeEventListener('abort', abortHandler) 25 + }) 27 26 }
+7 -7
src/common/breaker.ts
··· 1 - /** @module common/async */ 2 - 3 1 import { Callback } from "#common/types" 4 2 5 3 /** ··· 7 5 * the breaker is tripped. 8 6 * 9 7 * @example 8 + * ``` 10 9 * const breaker = makeBreaker() 11 10 * 12 11 * state.addEventHandler('finish', breaker.tripThen((e) => { ··· 23 22 * // this will only be allowed to run many times 24 23 * // but not *after* any of the _once_ wrappers has been called 25 24 * }) 25 + * ``` 26 26 */ 27 27 export class Breaker { 28 28 ··· 30 30 #onTripped?: () => void 31 31 32 32 /** 33 - * @param [onTripped] 34 - * an optional callback, called when the breaker is tripped, /before/ any wrapped functions. 33 + * @param onTripped - 34 + * an optional callback, called when the breaker is tripped, /before/ any wrapped functions. 35 35 */ 36 36 constructor(onTripped?: () => void) { 37 37 this.#tripped = false ··· 47 47 * wrap the given callback in a function that will trip the breaker before it's called. 48 48 * any subsequent calls to the wrapped function will be no-ops. 49 49 * 50 - * @param fn the function to be wrapped in the breaker 50 + * @param fn - the function to be wrapped in the breaker 51 51 * @returns a wrapped function, controlled by the breaker 52 52 */ 53 53 tripThen<CB extends Callback>(fn: CB): CB { ··· 66 66 * wrap the given callback in a function that check the breaker before it's called. 67 67 * once the breaker has been tripped, calls to the wrapped function will be no-ops. 68 68 * 69 - * @param {common_types.Callback} fn the function to be wrapped in the breaker 70 - * @returns {common_types.Callback} a wrapped function, controlled by the breaker 69 + * @param fn - the function to be wrapped in the breaker 70 + * @returns a wrapped function, controlled by the breaker 71 71 */ 72 72 untilTripped<CB extends Callback>(fn: CB): CB { 73 73 return ((...args: Parameters<CB>): void => {
+11 -15
src/common/crypto/cipher.ts
··· 1 - /** @module common/crypto */ 2 - 3 1 import { base64url } from 'jose' 4 2 import { nanoid } from 'nanoid' 5 3 ··· 25 23 * Derive a key given PBKDF inputs; so long as all of the inputs are stable, the key will 26 24 * be the same across derivations. 27 25 * 28 - * @private 29 - * 30 - * @param passwordStr a password for derivation 31 - * @param saltStr a salt for derivation 32 - * @param nonceStr a nonce for derivation 33 - * @param [iterations] number of iterations for pbkdf 26 + * @param passwordStr - a password for derivation 27 + * @param saltStr - a salt for derivation 28 + * @param nonceStr - a nonce for derivation 29 + * @param iterations - number of iterations for pbkdf 34 30 * @returns the derived crypto key 35 31 */ 36 32 async function deriveKey(passwordStr: string, saltStr: string, nonceStr: string, iterations: number = 100000): Promise<CryptoKey> { ··· 68 64 * any missing parameter (password/salt/nonce) is replaced with a random value, 69 65 * but if a stable password/salt/nonce is given, the derived keys will be stable. 70 66 * 71 - * @param [passwordStr] a password for derivation 72 - * @param [saltStr] a salt for derivation 73 - * @param [nonceStr] a nonce for derivation 67 + * @param passwordStr - a password for derivation 68 + * @param saltStr - a salt for derivation 69 + * @param nonceStr - a nonce for derivation 74 70 * @returns the derived {@link Cipher} 75 71 */ 76 72 static async derive(passwordStr: string, saltStr: string, nonceStr: string): Promise<Cipher> { ··· 84 80 * import a cipher from an aleady existing {@link CryptoKey}. 85 81 * does _not_ ensure that the imported key will work with our preferred encryption 86 82 * 87 - * @param cryptokey the key to import into a Cipher 83 + * @param cryptokey - the key to import into a Cipher 88 84 */ 89 85 constructor(cryptokey: CryptoKey) { 90 86 this.#cryptokey = cryptokey 91 87 } 92 88 93 89 /** 94 - * @param data the data to encrypte 90 + * @param data - the data to encrypte 95 91 * @returns a url-safe base64 encoded encrypted string. 96 92 */ 97 93 async encrypt(data: (string | Uint8Array)): Promise<string> { ··· 109 105 } 110 106 111 107 /** 112 - * @param encryptedData a base64 encoded string, previously encrypted with this cipher. 108 + * @param encryptedData - a base64 encoded string, previously encrypted with this cipher. 113 109 * @returns the decrypted output, decoded into utf-8 text. 114 110 */ 115 111 async decryptText(encryptedData: string): Promise<string> { ··· 118 114 } 119 115 120 116 /** 121 - * @param encryptedData a base64 encoded string, previously encrypted with this cipher. 117 + * @param encryptedData - a base64 encoded string, previously encrypted with this cipher. 122 118 * @returns the decrypted output, as an array buffer of bytes. 123 119 */ 124 120 async decryptBytes(encryptedData: string): Promise<ArrayBuffer> {
+1 -5
src/common/crypto/jwks.ts
··· 1 - /** @module common/crypto */ 2 - 3 1 import * as jose from 'jose' 4 2 import { z } from 'zod/v4' 5 3 import { CryptoError } from './errors.js' 6 - 7 - /** @typedef {jose.JWK} JWK */ 8 4 9 5 const subtleSignAlgo = { name: 'ECDSA', namedCurve: 'P-256' } 10 6 const joseSignAlgo = { name: 'ES256' } ··· 105 101 } 106 102 107 103 /** 108 - * @param payload the payload to sign 104 + * @param payload - the payload to sign 109 105 * @returns a properly configured jwt signer, with the payload provided 110 106 */ 111 107 export function generateSignableJwt(payload: jose.JWTPayload): jose.SignJWT {
+4 -6
src/common/crypto/jwts.ts
··· 1 - /** @module common/crypto */ 2 - 3 1 import * as jose from 'jose' 4 2 import { z } from 'zod/v4' 5 3 import { JWTBadSignatureError } from '#common/crypto/errors' ··· 61 59 type VerifyOpts = Partial<Omit<jose.JWTVerifyOptions, 'algorithms'>> 62 60 63 61 /** 64 - * @param jwt the (still encoded) token to verify 65 - * @param pubkey the key with which to verify the token 66 - * @param [options] the key with which to verify the token 62 + * @param jwt - the (still encoded) token to verify 63 + * @param pubkey - the key with which to verify the token 64 + * @param options - the key with which to verify the token 67 65 * @returns a verified payload 68 66 * @throws if the signature is not valid 69 67 */ ··· 85 83 /** 86 84 * generate a fingerprint for the given crypto key 87 85 * 88 - * @param key the key to fingerprint 86 + * @param key - the key to fingerprint 89 87 * @returns the sha256 fingerprint of the key 90 88 */ 91 89 export async function fingerprintKey(key: CryptoKey): Promise<string> {
-2
src/common/errors.ts
··· 1 - /** @module common */ 2 - 3 1 import { prettifyError, ZodError } from 'zod/v4' 4 2 5 3 const StatusCodes: Record<number, string> = {
+1 -3
src/common/protocol.ts
··· 1 - /** @module common/protocol */ 2 - 3 1 export * from './protocol/brands' 4 2 export * from './protocol/messages' 5 3 export * from './protocol/messages-preauth' ··· 11 9 export const parseJson: z.ZodTransform<unknown, string> = z.transform( 12 10 (input, ctx) => { 13 11 try { 14 - return JSON.parse(input) 12 + return JSON.parse(input) as unknown 15 13 } 16 14 catch { 17 15 ctx.issues.push({
-2
src/common/protocol/messages-preauth.ts
··· 1 - /** @module common/protocol */ 2 - 3 1 import { z } from 'zod/v4' 4 2 import { jwkSchema } from '#common/crypto/jwks' 5 3
+1 -3
src/common/schema/brand.ts
··· 1 - /** @module common/schema */ 2 - 3 1 import { nanoid } from 'nanoid' 4 2 import { z } from 'zod/v4' 5 3 ··· 37 35 return this.#schema.parse(input) as Branded<string, B> 38 36 } 39 37 40 - /** @return a boolean if the string is valid */ 38 + /** @returns a boolean if the string is valid */ 41 39 validate(input: string): input is Branded<string, B> { 42 40 return input != null && typeof input === 'string' && this.#schema.safeParse(input).success 43 41 }
+19 -14
src/common/socket.ts
··· 1 - /** @module common/socket */ 1 + import WebSocket, { ErrorEvent, MessageEvent } from 'isomorphic-ws' 2 2 3 3 import { combineSignals } from '#common/async/aborts' 4 4 import { BlockingAtom } from '#common/async/blocking-atom' ··· 18 18 * Given a websocket, wait and take a single message off and return it. 19 19 * 20 20 * @example 21 + * ``` 21 22 * const ws = new WebSocket("wss://example.com/stream") 22 23 * const timeout = timeoutSignal(5000) 23 24 * ··· 29 30 * if (ws.readyState !== ws.CLOSED) 30 31 * ws.close(); 31 32 * } 33 + * ``` 32 34 */ 33 35 export async function takeSocket(ws: WebSocket, signal?: AbortSignal): Promise<unknown> { 34 36 signal?.throwIfAborted() ··· 39 41 const error = new AbortController() 40 42 const multisignal = combineSignals(error.signal, signal) 41 43 42 - const onMessage = breaker.tripThen(m => atom.set(m.data)) 43 - const onError = breaker.tripThen(e => error.abort(e)) 44 - const onClose = breaker.tripThen(() => error.abort('closed')) 44 + const onMessage = breaker.tripThen((m: MessageEvent) => { atom.set(m.data); }) 45 + const onError = breaker.tripThen((e: unknown) => { error.abort(e); }) 46 + const onClose = breaker.tripThen(() => { error.abort('closed'); }) 45 47 46 48 try { 47 49 ws.addEventListener('message', onMessage) ··· 50 52 51 53 const data = await atom.get(multisignal) 52 54 if (!data) { 53 - throw new ProtocolError('socket read aborted', 408, multisignal?.reason) 55 + const cause = normalizeError(multisignal.reason) 56 + throw new ProtocolError('socket read aborted', 408, { cause }) 54 57 } 55 58 56 59 return data ··· 65 68 /** 66 69 * exactly take socket, but will additionally apply a json decoding 67 70 * 68 - * @param ws the socket to read 69 - * @param schema an a schema to execute 70 - * @param [signal] an abort signal to cancel the block 71 + * @param ws - the socket to read 72 + * @param schema - an a schema to execute 73 + * @param signal - an abort signal to cancel the block 71 74 * @returns the message off the socket 72 75 */ 73 - export async function takeSocketJson<T>(ws: WebSocket, schema: z.ZodSchema<T>, signal?: AbortSignal): Promise<T> { 76 + export async function takeSocketJson<T>(ws: WebSocket, schema: z.ZodType<T>, signal?: AbortSignal): Promise<T> { 74 77 const data = await takeSocket(ws, signal) 75 78 return parseJson.pipe(schema).parseAsync(data) 76 79 } ··· 101 104 * Given a websocket, stream messages off the socket as an async generator. 102 105 * 103 106 * @example 107 + * ```ts 104 108 * const ws = new WebSocket("wss://example.com/stream") 105 109 * const timeout = timeoutSignal(5000) 106 110 * ··· 114 118 * if (ws.readyState !== ws.CLOSED) 115 119 * ws.close(); 116 120 * } 121 + * ``` 117 122 */ 118 123 export async function* streamSocket(ws: WebSocket, config_?: Partial<ConfigProps>) { 119 124 const { signal, ...config } = { ...STREAM_CONFIG_DEFAULT, ...(config_ || {}) } ··· 123 128 const queue = new BlockingQueue<StreamYield>(config.maxDepth) 124 129 125 130 // if true, we're ignoring incoming messages until we drop the queue 126 - let inBackoffMode = false 131 + let inBackoffMode: boolean = false 127 132 const backoffThresh = Math.floor(config.maxDepth * 0.9) 128 133 129 134 // we don't want to keep processing after we've been closed ··· 145 150 }) 146 151 147 152 // todo: why are we getting this on client shutdown instead of onClose? 148 - const onError = breaker.tripThen((e: Event) => { 149 - queue.enqueue([error$, normalizeError(e)]) 153 + const onError = breaker.tripThen((e: ErrorEvent) => { 154 + queue.enqueue([error$, normalizeError(e.error)]) 150 155 }) 151 156 152 157 const onClose = breaker.tripThen(() => { ··· 190 195 * exactly stream socket, but will additionally apply a json decoding 191 196 * messages not validating will end the stream with an error 192 197 */ 193 - export async function* streamSocketJson(ws: WebSocket, config?: Partial<ConfigProps>): AsyncGenerator<unknown> { 198 + export async function* streamSocketJson(ws: WebSocket, config?: Partial<ConfigProps>): AsyncGenerator { 194 199 for await (const message of streamSocket(ws, config)) { 195 200 yield parseJson.parseAsync(message) 196 201 } ··· 202 207 */ 203 208 export async function* streamSocketSchema<T>( 204 209 ws: WebSocket, 205 - schema: z.ZodSchema<T>, 210 + schema: z.ZodType<T>, 206 211 config?: Partial<ConfigProps>, 207 212 ): AsyncGenerator<T> { 208 213 const parser = parseJson.pipe(schema)
+3 -3
src/common/strict-map.ts
··· 1 - /** @module common */ 2 - 3 1 /** A map with methods to ensure key presence and safe update. */ 4 2 export class StrictMap<K, V> extends Map<K, V> { 5 3 6 4 /** 7 5 * Get a value from the map, throwing if missing 8 - * @throws {Error} if the key is not present in the map 6 + * @throws Error if the key is not present in the map 9 7 */ 10 8 require(key: K): V { 11 9 if (!this.has(key)) throw Error(`key is required but not in the map`) 12 10 11 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 13 12 const value = this.get(key)! 14 13 return value 15 14 } ··· 20 19 this.set(key, maker()) 21 20 } 22 21 22 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 23 23 return this.get(key)! 24 24 } 25 25
+2 -5
src/common/types.ts
··· 1 - /** @module common */ 2 - 3 1 import { NEVER } from 'zod/v4' 4 2 5 - /** 6 - * A callback function, with arbitrary arguments; use {Parameters} to extract them. 7 - */ 3 + /** A callback function, with arbitrary arguments; use `Parameters` to extract them. */ 4 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 5 export type Callback = (...args: any[]) => void 9 6 10 7 /**
+11 -3
src/server/index.ts
··· 1 1 import express from 'express' 2 2 import * as http from 'http' 3 + 3 4 import { WebSocketServer } from 'ws' 4 5 5 6 import { apiRouter } from './routes-api/middleware' ··· 10 11 /** 11 12 * configures an http server which hosts the SPA and websocket endpoint 12 13 * 13 - * @param root the path to the root public/ directory 14 + * @param root - the path to the root public/ directory 14 15 * @returns a configured server 15 16 */ 16 - export function buildServer(root: string): http.Server<typeof http.IncomingMessage> { 17 + export function buildServer(root: string): http.Server { 17 18 const app = express() 19 + 20 + // not sure why this error is coming up 21 + // eslint-disable-next-line @typescript-eslint/no-misused-promises 18 22 const server = http.createServer(app) 19 23 20 24 // API routes ··· 29 33 30 34 // WebSocket handling 31 35 const wss = new WebSocketServer({ server, path: '/stream' }) 32 - wss.on('connection', socketHandler) 36 + wss.on('connection', (ws) => { 37 + socketHandler(ws) 38 + .catch((e: unknown) => { console.error('uncaught error from websocket', e) }) 39 + .finally(() => { console.log('socket handler complete') }) 40 + }) 33 41 34 42 return server 35 43 }
+3 -1
src/server/routes-socket/handler-preauth.ts
··· 1 + import WebSocket from 'isomorphic-ws' 2 + 1 3 import { combineSignals, timeoutSignal } from '#common/async/aborts' 2 4 import { jwkImport } from '#common/crypto/jwks' 3 5 import { jwtPayload, verifyJwtToken } from '#common/crypto/jwts' ··· 30 32 realms.ensureRegisteredRealm(realmid, identid, registrantkey) 31 33 } 32 34 33 - return authenticatePreauth(realmid, identid, jwt.token) 35 + return await authenticatePreauth(realmid, identid, jwt.token) 34 36 } 35 37 finally { 36 38 timeout.cancel()
+11 -10
src/server/routes-socket/handler-realm.ts
··· 1 + import { WebSocket } from 'isomorphic-ws' 2 + 1 3 import { normalizeProtocolError, ProtocolError } from '#common/errors' 2 - 4 + import { sendSocket, streamSocket } from '#common/socket.js' 3 5 import * as protocol from '#common/protocol' 4 - import { sendSocket, streamSocket } from '#common/socket.js' 5 6 import * as realm from '#server/routes-socket/state' 6 7 7 8 /** ··· 29 30 continue 30 31 31 32 default: 32 - throw new ProtocolError(`unknown message type: ${msg}`, 400) 33 + console.error('unknown message!', msg) 34 + throw new ProtocolError(`unknown message type!`, 400) 33 35 } 34 36 } 35 37 catch (exc) { ··· 82 84 } 83 85 84 86 /** 85 - * @private 86 - * @param {realm_types.AuthenticatedConnection} auth the current identity 87 - * @param {unknown} payload the payload to send 88 - * @param {protocol_types.IdentID[] | boolean} [recipients] 89 - * when true, send to the whole realm, including self 90 - * when false, send to the whole realm, excluding self 91 - * when an array of recipients, send to those recipients explicitly 87 + * @param auth - the current identity 88 + * @param payload - the payload to send 89 + * @param recipients - an optional list of recpipents 90 + * when true, send to the whole realm, including self 91 + * when false, send to the whole realm, excluding self 92 + * when an array of recipients, send to those recipients explicitly 92 93 */ 93 94 function realmBroadcast(auth: realm.AuthenticatedIdentity, payload: unknown, recipients: protocol.IdentID[] | boolean = false) { 94 95 const echo = recipients === true || Array.isArray(recipients)
+1
src/server/routes-socket/handler.ts
··· 1 1 import { format } from 'node:util' 2 + import WebSocket from 'isomorphic-ws' 2 3 3 4 import { normalizeError, normalizeProtocolError } from '#common/errors' 4 5
+5 -3
src/server/routes-socket/state.ts
··· 1 + import WebSocket from 'isomorphic-ws' 2 + 1 3 import { IdentID, RealmID } from '#common/protocol.js' 2 4 import { StrictMap } from '#common/strict-map' 3 5 ··· 22 24 * as initial registrants in a newly created realm. If the realm already 23 25 * exists, it's not changed. 24 26 * 25 - * @param realmid the realm id to ensure exists 26 - * @param registrantid the identity id of the registrant 27 - * @param registrantkey the public key of the registrant 27 + * @param realmid - the realm id to ensure exists 28 + * @param registrantid - the identity id of the registrant 29 + * @param registrantkey - the public key of the registrant 28 30 * @returns a registered realm, possibly newly created with the registrant 29 31 */ 30 32 export function ensureRegisteredRealm(realmid: RealmID, registrantid: IdentID, registrantkey: CryptoKey): Realm {
+3 -3
src/server/routes-static.ts
··· 9 9 /** 10 10 * returns a configured static middleware 11 11 * 12 - * @param opts options for corfiguring the middleware 12 + * @param opts - options for corfiguring the middleware 13 13 * @returns a new middleware 14 14 */ 15 15 export function makeStaticMiddleware(opts: StaticOpts): express.RequestHandler { ··· 19 19 /** 20 20 * returns the index file for any GET request for text/html it matches 21 21 * 22 - * @param opts options for configuring the middleware 22 + * @param opts - options for configuring the middleware 23 23 * @returns a new middleware 24 24 */ 25 25 export function makeSpaMiddleware(opts: StaticOpts): express.RequestHandler { 26 26 return (req, res, next) => { 27 27 if (req.method === 'GET' && req.accepts('text/html')) { 28 - return res.sendFile(join(opts.root, opts.index)) 28 + res.sendFile(join(opts.root, opts.index)); return; 29 29 } 30 30 31 31 next() // otherwise