From b7b2192b5a40f3be81deefa3470a73002dc7c0e0 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:36:34 +0100 Subject: [PATCH 01/10] chore: scaffold the @qauth-labs/plugin-github package TypeScript (strict, ESM, Node 22+), ESLint with type-checked rules, Prettier and Vitest. The package depends only on @qauth-labs/plugin-sdk from npm, pinned to the release candidate it is written against, and takes zod as a peer so it shares the server's copy. The lint rules forbid fetch and the Node network modules in src/: every outbound call goes through the egress client the server hands the plugin. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- .gitignore | 6 + .prettierignore | 5 + .prettierrc.json | 5 + eslint.config.js | 70 ++ package-lock.json | 2502 +++++++++++++++++++++++++++++++++++++++++++ package.json | 69 ++ tsconfig.build.json | 15 + tsconfig.json | 20 + vitest.config.ts | 10 + 9 files changed, 2702 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f24e92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +*.tgz +# The end-to-end suite's generated certificates, allowlist and logs. +e2e/.work/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f0de231 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist/ +node_modules/ +coverage/ +package-lock.json +CHANGELOG.md diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ae7544d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "printWidth": 100, + "trailingComma": "es5" +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..961d0af --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,70 @@ +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist/', 'node_modules/', 'coverage/'] }, + js.configs.recommended, + ...tseslint.configs.strictTypeChecked, + { + languageOptions: { + parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }, + }, + }, + { + rules: { + // The AuthMethod contract's members are async by their type; one that + // has nothing to wait for is still async. + '@typescript-eslint/require-await': 'off', + }, + }, + { + files: ['src/**/*.ts'], + rules: { + // Every outbound call goes through the egress client the server hands + // the plugin (ADR-019 Decision 8), never a network API of its own. + 'no-restricted-globals': [ + 'error', + { + name: 'fetch', + message: 'Use context.egress: every call goes through the egress client.', + }, + ], + 'no-restricted-imports': [ + 'error', + { + paths: [ + 'node:http', + 'node:https', + 'node:net', + 'node:tls', + 'http', + 'https', + 'net', + 'tls', + 'undici', + ].map((name) => ({ + name, + message: 'Use context.egress: every call goes through the egress client.', + })), + }, + ], + }, + }, + { + files: ['**/*.js', '**/*.mjs'], + ...tseslint.configs.disableTypeChecked, + }, + { + // The plain-JavaScript files (this config, the mock GitHub) run on Node. + files: ['**/*.js', '**/*.mjs'], + languageOptions: { + globals: Object.fromEntries( + ['Buffer', 'URL', 'URLSearchParams', 'console', 'process', 'structuredClone'].map( + (name) => [name, 'readonly'] + ) + ), + }, + }, + prettier +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b759829 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2502 @@ +{ + "name": "@qauth-labs/plugin-github", + "version": "0.1.0-rc.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@qauth-labs/plugin-github", + "version": "0.1.0-rc.0", + "license": "Apache-2.0", + "dependencies": { + "@qauth-labs/plugin-sdk": "0.2.0-rc.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^22.19.0", + "eslint": "^10.14.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.72.1", + "vitest": "^5.1.0", + "zod": "^4.6.7" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^4.6.7" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@qauth-labs/plugin-sdk": { + "version": "0.2.0-rc.1", + "resolved": "https://registry.npmjs.org/@qauth-labs/plugin-sdk/-/plugin-sdk-0.2.0-rc.1.tgz", + "integrity": "sha512-HzJdOkoteT1sd8y6iOQ0HVpdBNB1kNuSXQFlhu6CwS8ZEFgRN05Hrx+TLBzQagwig7fXGAQVxj37khOJbjxS/w==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^4.6.7" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz", + "integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz", + "integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz", + "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.6", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.156.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" + } + }, + "node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.3.tgz", + "integrity": "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", + "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.32", + "rolldown": "~1.2.16", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz", + "integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.1.0", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.1.0", + "@vitest/browser-preview": "5.1.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.1.0", + "@vitest/coverage-v8": "5.1.0", + "@vitest/ui": "5.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c489671 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "@qauth-labs/plugin-github", + "version": "0.1.0-rc.0", + "description": "GitHub sign-in for QAuth, as an AuthMethod plugin: OAuth 2.0 with PKCE and GitHub's user API, keyed by the numeric GitHub user id. Experimental.", + "keywords": [ + "qauth", + "qauth-plugin", + "auth-method", + "github", + "oauth", + "sign-in" + ], + "license": "Apache-2.0", + "homepage": "https://github.com/qauth-labs/plugin-github#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/qauth-labs/plugin-github.git" + }, + "bugs": { + "url": "https://github.com/qauth-labs/plugin-github/issues" + }, + "type": "module", + "sideEffects": false, + "engines": { + "node": ">=22" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", + "e2e": "vitest run --config e2e/vitest.config.ts", + "prepack": "npm run build" + }, + "dependencies": { + "@qauth-labs/plugin-sdk": "0.2.0-rc.1" + }, + "peerDependencies": { + "zod": "^4.6.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^22.19.0", + "eslint": "^10.14.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.72.1", + "vitest": "^5.1.0", + "zod": "^4.6.7" + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..a70b780 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + // The published build: Node's ESM with declarations, from src/ into dist/. + // A published plugin ships JavaScript: Node does not strip TypeScript types + // under node_modules. + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "sourceMap": false + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ad77aa3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + // Type-checks everything: the plugin, its tests and the end-to-end suite. + // The published build is tsconfig.build.json. + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "types": ["node"], + "skipLibCheck": false, + "noEmit": true + }, + "include": ["src", "test", "e2e", "*.ts"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0d7ce17 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +// The unit tests and the contract test kit. The end-to-end suite has its own +// config (e2e/vitest.config.ts): it needs the containers. +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + environment: 'node', + }, +}); From f9aaac21095bfc4258b4ab24e32318b5f5c56a96 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:36:53 +0100 Subject: [PATCH 02/10] feat: sign in with GitHub as a QAuth AuthMethod The authorization code flow with a fresh state and PKCE (S256) on a callback URL of this provider alone, the code redeemed at the token endpoint asking for JSON, then GET /user and GET /user/emails with the user:email scope only. The account is the numeric user id under the GitHub instance that issued it, never the login. The email is verified only when GitHub marks it primary and verified; the login and the display name are unverified attributes. The access token is used for those two reads and dropped. A baseUrl selects a GitHub Enterprise Server; its calls carry the admin source, so the operator's egress allowlist decides whether a private address is reached. Calls that carry a credential follow no redirect. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- src/config.ts | 91 ++++++++++++++++++ src/github.ts | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 21 ++++ src/method.ts | 229 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 600 insertions(+) create mode 100644 src/config.ts create mode 100644 src/github.ts create mode 100644 src/index.ts create mode 100644 src/method.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..98292cb --- /dev/null +++ b/src/config.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; + +/** + * The plugin's config for one realm, and where GitHub answers for it. + * + * Without `baseUrl` the plugin signs people in at github.com. With it, at a + * GitHub Enterprise Server: its web pages at the base URL, its REST API under + * `/api/v3` there. A base URL that resolves to a private address works only + * when the operator's egress allowlist names it for the realm: the plugin + * makes every call through the egress client the server hands it. + */ + +/** github.com: its web origin, which is also the issuer of its user ids. */ +export const GITHUB_COM = 'https://github.com'; + +/** github.com's REST API. */ +export const GITHUB_COM_API = 'https://api.github.com'; + +/** + * A base URL: `https://` and a host name or IPv4 address, an optional port, + * an optional trailing `/`, and nothing else (no credentials, path, query or + * fragment). GitHub Enterprise Server answers at the root of its host. + */ +export const BASE_URL_PATTERN = + /^https:\/\/[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?(?::[0-9]{1,5})?\/?$/; + +/** The config an admin gives the plugin for one realm. */ +export const configSchema = z.object({ + /** The client id of the GitHub OAuth app (or GitHub App) registered for this realm. */ + clientId: z + .string() + .min(1) + .max(255) + .regex(/^[\x21-\x7e]+$/), + /** Its client secret. Write-only: the server stores it encrypted and never shows it again. */ + clientSecret: z.string().min(1).max(1024).meta({ writeOnly: true }), + /** + * The base URL of a GitHub Enterprise Server, such as + * `https://github.example.com`. Leave it out for github.com. + */ + baseUrl: z + .url({ protocol: /^https$/ }) + .max(270) + .regex(BASE_URL_PATTERN) + .optional(), +}); + +/** The parsed config. */ +export type GitHubConfig = z.output; + +/** Where one GitHub answers. */ +export interface GitHubEndpoints { + /** + * The issuer of the user ids: `https://github.com`, or the origin of the + * Enterprise Server. A numeric id is unique only within it. + */ + readonly issuer: string; + /** The page the browser is sent to. */ + readonly authorize: string; + /** Where the code is redeemed. */ + readonly token: string; + /** The REST API's root, without a trailing `/`. */ + readonly api: string; + /** + * Who set the destinations, for the egress client: `operator` for + * github.com when the config names no base URL (this package's own code + * names it), `admin` for a base URL the realm's config names. + */ + readonly source: 'operator' | 'admin'; +} + +/** The endpoints of the GitHub `config` names. */ +export function endpointsOf(config: Pick): GitHubEndpoints { + const origin = config.baseUrl === undefined ? GITHUB_COM : new URL(config.baseUrl).origin; + if (origin === GITHUB_COM) { + return { + issuer: GITHUB_COM, + authorize: `${GITHUB_COM}/login/oauth/authorize`, + token: `${GITHUB_COM}/login/oauth/access_token`, + api: GITHUB_COM_API, + source: config.baseUrl === undefined ? 'operator' : 'admin', + }; + } + return { + issuer: origin, + authorize: `${origin}/login/oauth/authorize`, + token: `${origin}/login/oauth/access_token`, + api: `${origin}/api/v3`, + source: 'admin', + }; +} diff --git a/src/github.ts b/src/github.ts new file mode 100644 index 0000000..275b91d --- /dev/null +++ b/src/github.ts @@ -0,0 +1,259 @@ +import type { + AuthMethodContext, + AuthMethodEgressRequest, + AuthMethodEgressResponse, + UpstreamFailureReason, +} from '@qauth-labs/plugin-sdk'; +import { z } from 'zod'; + +import { endpointsOf, type GitHubConfig } from './config.js'; + +/** + * GitHub's side of a sign-in: redeem the code, then read the person with the + * access token. GitHub offers no OpenID Connect sign-in (no ID token, no + * `nonce`), so the person is whoever the REST API says the token belongs to. + * + * Every call goes through the egress client the server hands the plugin + * (`context.egress`), never `fetch`. Each call that carries a credential (the + * code and the client secret, the access token) follows no redirect, so a + * `307` cannot send the credential on. The access token is used for the two + * reads of one sign-in and then dropped: it is never stored, logged or handed + * to the server. + */ + +/** The scope the plugin asks for: the person's email addresses, read-only. Nothing else. */ +export const GITHUB_SCOPE = 'user:email'; + +/** The REST API version the plugin is written against. */ +export const GITHUB_API_VERSION = '2022-11-28'; + +/** GitHub refuses an API call without a `User-Agent`. */ +export const USER_AGENT = 'qauth-labs-plugin-github'; + +/** The most email addresses one page of `/user/emails` holds (GitHub's own maximum). */ +const EMAILS_PER_PAGE = 100; + +/** An OAuth error code as it may be logged: printable ASCII, short. */ +const LOGGABLE_ERROR_CODE = /^[\x20-\x21\x23-\x5b\x5d-\x7e]{1,64}$/; + +/** A sign-in that cannot go on, with the reason the server is told. */ +export class GitHubFailure extends Error { + override readonly name = 'GitHubFailure'; + constructor( + readonly reason: UpstreamFailureReason, + message: string + ) { + super(message); + } +} + +/** The person as GitHub's REST API describes them. */ +export interface GitHubPerson { + /** The numeric user id, as a decimal string: the subject. Never changes. */ + readonly id: string; + /** The login name. It can change, so it is never the subject. */ + readonly login: string; + /** The display name, when the person set one. */ + readonly name: string | undefined; + /** The primary email address, when GitHub lists exactly one. */ + readonly email: string | undefined; + /** Whether GitHub verified that primary address. */ + readonly emailVerified: boolean; +} + +const tokenAnswerSchema = z.object({ + access_token: z.string().min(1).max(4096), + token_type: z.string().optional(), +}); + +const userSchema = z.object({ + id: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), + login: z.string().min(1).max(255), + name: z.string().nullish(), +}); + +const emailsSchema = z + .array( + z.object({ + email: z.string().min(3).max(320), + primary: z.boolean(), + verified: z.boolean(), + }) + ) + .max(EMAILS_PER_PAGE); + +/** The JSON of an answer's body, or `undefined` when it is not JSON. */ +function jsonBody(answer: AuthMethodEgressResponse): unknown { + try { + return JSON.parse(Buffer.from(answer.body).toString('utf8')) as unknown; + } catch { + return undefined; + } +} + +/** The `error` member of an OAuth error answer, as it may be logged. */ +function loggableError(body: unknown): string { + const error = + typeof body === 'object' && body !== null + ? (body as Record)['error'] + : undefined; + return typeof error === 'string' && LOGGABLE_ERROR_CODE.test(error) ? error : 'no error code'; +} + +/** Send one call; a refused or failed call means GitHub cannot be reached. */ +async function send( + context: AuthMethodContext, + request: AuthMethodEgressRequest +): Promise { + try { + return await context.egress.request(request); + } catch { + // The egress client's error can quote the URL; the purpose says enough. + context.log.warn( + { purpose: request.purpose }, + 'GitHub could not be reached, or the egress client refused the call' + ); + throw new GitHubFailure('upstream_unavailable', `${request.purpose}: no answer`); + } +} + +/** Throw for a status that is not `200`: `429` and `5xx` mean GitHub is unavailable. */ +function checkStatus( + context: AuthMethodContext, + answer: AuthMethodEgressResponse, + purpose: string +): void { + if (answer.status === 200) return; + context.log.warn({ purpose, status: answer.status }, 'GitHub answered a status that is not 200'); + if (answer.status === 429 || answer.status >= 500) { + throw new GitHubFailure('upstream_unavailable', `${purpose}: ${String(answer.status)}`); + } + throw new GitHubFailure('invalid_response', `${purpose}: ${String(answer.status)}`); +} + +/** + * Redeem the code at GitHub's token endpoint, asking for JSON; the access + * token. The request carries the PKCE verifier, and the same `redirect_uri` + * the authorization request named. + */ +export async function redeemCode( + context: AuthMethodContext, + code: string, + codeVerifier: string, + redirectUri: string +): Promise { + const { config } = context; + const endpoints = endpointsOf(config); + const purpose = 'upstream-token'; + const answer = await send(context, { + method: 'POST', + url: endpoints.token, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + accept: 'application/json', + 'user-agent': USER_AGENT, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + client_id: config.clientId, + client_secret: config.clientSecret, + }).toString(), + purpose, + source: endpoints.source, + // The body carries the code and the secret: a 307 must not send it on. + redirect: 'error', + }); + if (answer.status === 429 || answer.status >= 500) { + checkStatus(context, answer, purpose); + } + const body = jsonBody(answer); + // GitHub answers an OAuth error with 200 and an `error` member. + if ( + (typeof body === 'object' && body !== null && 'error' in body) || + answer.status === 400 || + answer.status === 401 || + answer.status === 403 + ) { + const code = loggableError(body); + context.log.warn({ purpose, status: answer.status, error: code }, 'GitHub refused the code'); + throw new GitHubFailure('provider_error', `${purpose}: ${code}`); + } + checkStatus(context, answer, purpose); + const token = tokenAnswerSchema.safeParse(body); + if (!token.success) { + throw new GitHubFailure('invalid_response', `${purpose}: no access token`); + } + if (token.data.token_type !== undefined && token.data.token_type.toLowerCase() !== 'bearer') { + throw new GitHubFailure('invalid_response', `${purpose}: not a bearer token`); + } + return token.data.access_token; +} + +/** One read of GitHub's REST API with the access token; its JSON. */ +async function readApi( + context: AuthMethodContext, + accessToken: string, + path: string, + purpose: string +): Promise { + const endpoints = endpointsOf(context.config); + const answer = await send(context, { + method: 'GET', + url: `${endpoints.api}${path}`, + headers: { + authorization: `Bearer ${accessToken}`, + accept: 'application/vnd.github+json', + 'x-github-api-version': GITHUB_API_VERSION, + 'user-agent': USER_AGENT, + }, + purpose, + source: endpoints.source, + // The request carries the access token: follow no redirect with it. + redirect: 'error', + }); + checkStatus(context, answer, purpose); + return jsonBody(answer); +} + +/** + * Read the person the access token belongs to: `GET /user` for the id, the + * login and the display name, and `GET /user/emails` (the `user:email` + * scope) for the primary address and whether GitHub verified it. + */ +export async function readPerson( + context: AuthMethodContext, + accessToken: string +): Promise { + const [userBody, emailsBody] = await Promise.all([ + readApi(context, accessToken, '/user', 'upstream-user'), + readApi( + context, + accessToken, + `/user/emails?per_page=${String(EMAILS_PER_PAGE)}`, + 'upstream-user-emails' + ), + ]); + const user = userSchema.safeParse(userBody); + if (!user.success) { + throw new GitHubFailure('invalid_response', 'upstream-user: not a GitHub user'); + } + const emails = emailsSchema.safeParse(emailsBody); + if (!emails.success) { + throw new GitHubFailure('invalid_response', 'upstream-user-emails: not a list of addresses'); + } + // GitHub marks one address primary. Only that one is taken, and it counts + // as verified only when GitHub says so; a list with none, or with two, + // gives no address at all. + const primary = emails.data.filter((entry) => entry.primary); + const chosen = primary.length === 1 ? primary[0] : undefined; + return { + id: String(user.data.id), + login: user.data.login, + name: user.data.name ?? undefined, + email: chosen?.email, + emailVerified: chosen?.verified === true, + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a427381 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +/** + * `@qauth-labs/plugin-github` — GitHub sign-in for QAuth, as an AuthMethod + * plugin. EXPERIMENTAL, like the plugin API it is written against. + * + * The operator installs the package with the deployment, lists it in + * `AUTH_METHOD_PLUGINS` and turns on `PLUGIN_API_ENABLED`; each realm then + * enables and configures it through the admin API. See the README. + * + * @packageDocumentation + */ +export { + BASE_URL_PATTERN, + configSchema, + endpointsOf, + GITHUB_COM, + GITHUB_COM_API, + type GitHubConfig, + type GitHubEndpoints, +} from './config.js'; +export { GITHUB_API_VERSION, GITHUB_SCOPE } from './github.js'; +export { authMethod, CALLBACK_PATH, GITHUB_TYPE, type GitHubTransaction } from './method.js'; diff --git a/src/method.ts b/src/method.ts new file mode 100644 index 0000000..c1992b5 --- /dev/null +++ b/src/method.ts @@ -0,0 +1,229 @@ +import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'; + +import { + AUTH_METHOD_CONTRACT, + type AuthMethod, + type AuthMethodParameters, + UPSTREAM_STEP_URN, + type UpstreamCallbackOutcome, + type UserAttribute, + type VerifiedIdentity, +} from '@qauth-labs/plugin-sdk'; + +import { configSchema, endpointsOf, type GitHubConfig } from './config.js'; +import { GITHUB_SCOPE, GitHubFailure, readPerson, redeemCode } from './github.js'; + +/** + * GitHub sign-in as a QAuth AuthMethod (contract `qauth-auth-method/1-rc.0`). + * + * 1. `start()` makes a fresh `state` and PKCE verifier for each sign-in, + * keeps both in the transaction the server holds, and sends the browser + * to GitHub's authorization page with the state, the verifier's S256 + * challenge and the `user:email` scope. The state is the binding: the + * server refuses a callback that does not carry it back before this code + * runs. + * 2. GitHub sends the browser back to `/upstream/github/callback`, a URL of + * this provider alone. GitHub issues no ID token and no `nonce`, so the + * state, checked against the flow cookie the start bound, and that URL are + * what tie the answer to the sign-in that asked for it. + * 3. The callback redeems the code (with the verifier), reads the person with + * the access token, and drops the token. The person is their numeric + * GitHub user id under the GitHub that issued it; the login name, which + * can change, is only an attribute. + */ + +/** The method's type: its route prefix (`/upstream/github/…`) and the source of its attributes. */ +export const GITHUB_TYPE = 'github'; + +/** The path of the callback, below the method's prefix. */ +export const CALLBACK_PATH = '/callback'; + +/** What `start()` asks the server to keep until the callback. */ +export interface GitHubTransaction { + readonly state: string; + /** The PKCE verifier (RFC 7636 §4.1): 32 random bytes, base64url. */ + readonly codeVerifier: string; +} + +/** What the callback hands `verify()`: never the access token. */ +interface GitHubResult { + readonly issuer: string; + readonly id: string; + readonly login: string; + readonly name: string | undefined; + readonly email: string | undefined; + readonly emailVerified: boolean; +} + +/** The results the callback produced and `verify()` has not used yet. */ +const issued = new WeakSet(); + +/** The longest authorization code the callback takes. */ +const MAX_CODE_LENGTH = 1024; + +/** The longest attribute value the server accepts. */ +const MAX_ATTRIBUTE_LENGTH = 8192; + +/** A control character an attribute value may not hold: C0 but tab and line breaks, and DEL. */ +// eslint-disable-next-line no-control-regex -- matching control characters is the point +const CONTROL_CHARACTER = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/; + +/** One parameter's single value; a missing or repeated one is `undefined`. */ +function single(parameters: AuthMethodParameters, name: string): string | undefined { + const value = parameters[name]; + return typeof value === 'string' ? value : undefined; +} + +/** The S256 code challenge of a PKCE verifier (RFC 7636 §4.2). */ +function s256(verifier: string): string { + return createHash('sha256').update(verifier, 'ascii').digest('base64url'); +} + +/** Constant-time string equality. */ +function sameText(left: string, right: string): boolean { + const a = Buffer.from(left, 'utf8'); + const b = Buffer.from(right, 'utf8'); + return a.length === b.length && timingSafeEqual(a, b); +} + +/** The transaction the server handed back, as `start()` made it. */ +function transactionOf(value: unknown): GitHubTransaction | undefined { + if (typeof value !== 'object' || value === null) return undefined; + const { state, codeVerifier } = value as Record; + return typeof state === 'string' && typeof codeVerifier === 'string' + ? { state, codeVerifier } + : undefined; +} + +/** Whether the server takes `value` as an attribute value. */ +function isAttributeValue(value: unknown): value is string { + return ( + typeof value === 'string' && + value.length > 0 && + value.length <= MAX_ATTRIBUTE_LENGTH && + !CONTROL_CHARACTER.test(value) && + (String.prototype as unknown as { isWellFormed(this: string): boolean }).isWellFormed.call( + value + ) + ); +} + +/** The plugin's method: what the package exports as `authMethod`. */ +export const authMethod: AuthMethod = { + contract: AUTH_METHOD_CONTRACT, + type: GITHUB_TYPE, + id: 'github', + display: { name: 'GitHub' }, + configSchema, + steps: [UPSTREAM_STEP_URN], + + async start(_request, context) { + const { config } = context; + // 32 random bytes each: 43 base64url characters. + const state = randomBytes(32).toString('base64url'); + const codeVerifier = randomBytes(32).toString('base64url'); + const url = new URL(endpointsOf(config).authorize); + // GitHub ignores `response_type`; it is sent for the OAuth 2.0 request's + // own shape (RFC 6749 §4.1.1). + url.searchParams.set('response_type', 'code'); + url.searchParams.set('client_id', config.clientId); + url.searchParams.set('redirect_uri', context.routeUrl(CALLBACK_PATH)); + url.searchParams.set('scope', GITHUB_SCOPE); + url.searchParams.set('state', state); + // github.com accepts PKCE with S256 for OAuth apps and GitHub Apps. A + // server that does not know the parameters ignores them (RFC 6749 §3.1). + url.searchParams.set('code_challenge', s256(codeVerifier)); + url.searchParams.set('code_challenge_method', 'S256'); + return { redirectTo: url.href, transaction: { state, codeVerifier }, binding: state }; + }, + + routes: [ + { + kind: 'callback', + method: 'GET', + path: CALLBACK_PATH, + // The server has matched `state` with this transaction's binding + // before this runs; it is compared once more here, for nothing. + async handle(request, context): Promise { + const transaction = transactionOf(request.transaction); + const state = single(request.query, 'state'); + if ( + transaction === undefined || + state === undefined || + !sameText(state, transaction.state) + ) { + return { kind: 'failed', reason: 'transaction_mismatch' }; + } + const error = single(request.query, 'error'); + if (error === 'access_denied') return { kind: 'declined' }; + if (error !== undefined) return { kind: 'failed', reason: 'provider_error' }; + const code = single(request.query, 'code'); + if (code === undefined || code.length === 0 || code.length > MAX_CODE_LENGTH) { + return { kind: 'failed', reason: 'invalid_response' }; + } + + let result: GitHubResult; + try { + const accessToken = await redeemCode( + context, + code, + transaction.codeVerifier, + context.routeUrl(CALLBACK_PATH) + ); + // The token is used for these two reads, then dropped. + const person = await readPerson(context, accessToken); + result = Object.freeze({ issuer: endpointsOf(context.config).issuer, ...person }); + } catch (failure) { + if (failure instanceof GitHubFailure) { + return { kind: 'failed', reason: failure.reason }; + } + throw failure; + } + issued.add(result); + return { kind: 'result', result }; + }, + }, + ], + + async verify(input: unknown): Promise { + if (typeof input !== 'object' || input === null || !issued.has(input)) { + throw new Error('not a result of the GitHub callback'); + } + // Single use: a result signs one person in once. + issued.delete(input); + const result = input as GitHubResult; + return { + // The numeric id, never the login: a login can change hands. + externalSub: result.id, + assuranceLevel: 'low', + rawClaims: { + // The GitHub that assigned the id: an id is unique only within it. + iss: result.issuer, + sub: result.id, + login: result.login, + ...(result.name === undefined ? {} : { name: result.name }), + ...(result.email === undefined + ? {} + : { email: result.email, email_verified: result.emailVerified }), + }, + }; + }, + + extractAttributes(identity: VerifiedIdentity): UserAttribute[] { + const claims = identity.rawClaims; + const claim = (name: string): unknown => + Object.hasOwn(claims, name) ? claims[name] : undefined; + const attributes: UserAttribute[] = []; + const add = (attrKey: string, value: unknown, verified: boolean): void => { + if (isAttributeValue(value)) { + attributes.push({ source: GITHUB_TYPE, attrKey, attrValue: value, verified }); + } + }; + // Verified only for the primary address GitHub marks verified. + add('email', claim('email'), claim('email_verified') === true); + // The login and the display name are the person's own words: never verified. + add('preferred_username', claim('login'), false); + add('name', claim('name'), false); + return attributes; + }, +}; From ada32e0f1b9e37135de087d2badb83baeec74bcf Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:36:53 +0100 Subject: [PATCH 03/10] test: unit tests and the SDK's contract test kit A fake GitHub that checks what GitHub checks (client, code, redirect_uri, PKCE verifier, bearer token, User-Agent) drives the method through renames, email changes, the primary and verified rules, refusals and outages, and the single use of a callback result. checkAuthMethod runs for github.com and for an Enterprise Server. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- test/contract.test.ts | 29 +++ test/fake-context.ts | 52 ++++++ test/fake-github.ts | 206 +++++++++++++++++++++ test/method.test.ts | 406 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 693 insertions(+) create mode 100644 test/contract.test.ts create mode 100644 test/fake-context.ts create mode 100644 test/fake-github.ts create mode 100644 test/method.test.ts diff --git a/test/contract.test.ts b/test/contract.test.ts new file mode 100644 index 0000000..4d7da8e --- /dev/null +++ b/test/contract.test.ts @@ -0,0 +1,29 @@ +/** + * The plugin meets the AuthMethod contract, as QAuth's server holds a plugin + * to it: the SDK's contract test kit, `checkAuthMethod`, with a fake GitHub + * playing the provider. + */ +import { AUTH_METHOD_CONTRACT, AUTH_METHOD_PLUGIN_EXPORT } from '@qauth-labs/plugin-sdk'; +import { checkAuthMethod } from '@qauth-labs/plugin-sdk/testing'; +import { describe, expect, it } from 'vitest'; + +import * as plugin from '../src/index.js'; +import { CONFIG, FakeGitHub } from './fake-github.js'; + +describe('the package', () => { + it('exports its method as authMethod, written against the SDK’s contract', () => { + expect(AUTH_METHOD_PLUGIN_EXPORT in plugin).toBe(true); + expect(plugin.authMethod.contract).toBe(AUTH_METHOD_CONTRACT); + }); + + it('meets the AuthMethod contract for github.com', async () => { + // Rejects with an AuthMethodContractError that lists every failed check. + const report = await checkAuthMethod(plugin.authMethod, new FakeGitHub().scenario()); + expect(report.checks.length).toBeGreaterThan(0); + }); + + it('meets the AuthMethod contract for a GitHub Enterprise Server', async () => { + const config = { ...CONFIG, baseUrl: 'https://github.corp.example' }; + await checkAuthMethod(plugin.authMethod, new FakeGitHub(config).scenario()); + }); +}); diff --git a/test/fake-context.ts b/test/fake-context.ts new file mode 100644 index 0000000..2fc1332 --- /dev/null +++ b/test/fake-context.ts @@ -0,0 +1,52 @@ +/** + * The context the server hands the method, faked for the unit tests: the + * config, the egress client (answered by a fake GitHub), a signer the plugin + * never needs, a logger that keeps every line, and `routeUrl` on a test + * realm. The contract test kit builds the server's own context instead. + */ +import type { + AuthMethodContext, + AuthMethodEgressRequest, + AuthMethodEgressResponse, +} from '@qauth-labs/plugin-sdk'; + +import type { GitHubConfig } from '../src/config.js'; + +/** The realm's origin in the unit tests. */ +export const REALM_ORIGIN = 'https://realm.test'; + +/** One log line the method wrote. */ +export interface LogLine { + readonly level: 'debug' | 'info' | 'warn' | 'error'; + readonly fields: Record; + readonly message: string; +} + +export interface FakeContext extends AuthMethodContext { + readonly lines: LogLine[]; +} + +export function fakeContext( + config: GitHubConfig, + egress: ( + request: AuthMethodEgressRequest + ) => AuthMethodEgressResponse | Promise +): FakeContext { + const lines: LogLine[] = []; + const logAt = + (level: LogLine['level']) => + (fields: Record, message: string): void => { + lines.push({ level, fields, message }); + }; + return { + config, + lines, + egress: { request: async (request) => egress(request) }, + signer: { + signClientAssertion: () => Promise.reject(new Error('the plugin signs nothing')), + getUpstreamJwks: () => Promise.reject(new Error('the plugin signs nothing')), + }, + log: { debug: logAt('debug'), info: logAt('info'), warn: logAt('warn'), error: logAt('error') }, + routeUrl: (path) => `${REALM_ORIGIN}/upstream/github${path}`, + }; +} diff --git a/test/fake-github.ts b/test/fake-github.ts new file mode 100644 index 0000000..adc650f --- /dev/null +++ b/test/fake-github.ts @@ -0,0 +1,206 @@ +/** + * A fake GitHub for the unit tests and the contract test kit: the + * authorization page, the token endpoint, `GET /user` and + * `GET /user/emails`, answering the plugin's egress calls as GitHub does + * (an OAuth error at the token endpoint is a `200` with an `error` member). + * It checks what GitHub checks: the client, the code, the `redirect_uri`, + * the PKCE verifier, the bearer token and the `User-Agent`. + */ +import { createHash, randomBytes } from 'node:crypto'; + +import type { AuthMethodEgressRequest, AuthMethodEgressResponse } from '@qauth-labs/plugin-sdk'; +import type { AuthMethodKitRequest, AuthMethodKitScenario } from '@qauth-labs/plugin-sdk/testing'; + +import { endpointsOf, type GitHubConfig } from '../src/config.js'; + +/** One email address as `GET /user/emails` lists it. */ +export interface FakeEmail { + email: string; + primary: boolean; + verified: boolean; + visibility?: 'public' | 'private' | null; +} + +/** The person signed in at the fake. Tests change it between sign-ins. */ +export interface FakePerson { + id: number; + login: string; + name: string | null; + emails: FakeEmail[]; +} + +/** The config the tests give the plugin, with a secret of realistic length. */ +export const CONFIG: GitHubConfig = { + clientId: 'Ov23liFakeClientId01', + clientSecret: 'fake-github-client-secret-0123456789abcdef', +}; + +/** What the fake knows about a code it issued. */ +interface IssuedCode { + readonly redirectUri: string; + readonly challenge: string | undefined; + readonly challengeMethod: string | undefined; +} + +function answer( + request: AuthMethodEgressRequest, + status: number, + body: unknown +): AuthMethodEgressResponse { + return { + status, + headers: { 'content-type': 'application/json; charset=utf-8' }, + body: new TextEncoder().encode(JSON.stringify(body)), + url: request.url, + }; +} + +function s256(verifier: string): string { + return createHash('sha256').update(verifier, 'ascii').digest('base64url'); +} + +export class FakeGitHub { + readonly person: FakePerson; + /** Every call the plugin made, in order. */ + readonly calls: AuthMethodEgressRequest[] = []; + /** Every access token the fake issued. */ + readonly tokens: string[] = []; + /** Answers that replace the fake's own, by path, for one test. */ + readonly overrides = new Map< + string, + (request: AuthMethodEgressRequest) => AuthMethodEgressResponse + >(); + private readonly codes = new Map(); + private readonly people = new Map(); + + constructor( + readonly config: GitHubConfig = CONFIG, + person?: Partial + ) { + this.person = { + id: 1_234_567, + login: 'octo-person', + name: 'Octo Person', + emails: [ + { email: 'octo@example.com', primary: true, verified: true, visibility: 'private' }, + { email: '1234567+octo-person@users.noreply.github.com', primary: false, verified: true }, + ], + ...person, + }; + } + + /** The person signs in on GitHub's authorization page; the code it redirects with. */ + authorize(authorizationRequest: URL): string { + const endpoints = endpointsOf(this.config); + if (`${authorizationRequest.origin}${authorizationRequest.pathname}` !== endpoints.authorize) { + throw new Error(`not the authorization page: ${authorizationRequest.href}`); + } + const query = authorizationRequest.searchParams; + if (query.get('client_id') !== this.config.clientId) throw new Error('another client'); + const redirectUri = query.get('redirect_uri'); + if (redirectUri === null) throw new Error('no redirect_uri'); + const code = randomBytes(10).toString('hex'); + this.codes.set(code, { + redirectUri, + challenge: query.get('code_challenge') ?? undefined, + challengeMethod: query.get('code_challenge_method') ?? undefined, + }); + return code; + } + + /** Answer one of the plugin's calls. */ + answer = (request: AuthMethodEgressRequest): AuthMethodEgressResponse => { + this.calls.push(request); + const endpoints = endpointsOf(this.config); + const url = new URL(request.url); + const override = this.overrides.get(url.pathname); + if (override !== undefined) return override(request); + if (request.headers?.['user-agent'] === undefined) { + return answer(request, 403, { message: 'Request forbidden by administrative rules.' }); + } + if (request.method === 'POST' && request.url === endpoints.token) return this.token(request); + if (request.method === 'GET' && url.origin + url.pathname === `${endpoints.api}/user`) { + const person = this.bearer(request); + if (person === undefined) return answer(request, 401, { message: 'Bad credentials' }); + return answer(request, 200, { + login: person.login, + id: person.id, + node_id: 'U_fake', + type: 'User', + name: person.name, + email: null, + }); + } + if (request.method === 'GET' && url.origin + url.pathname === `${endpoints.api}/user/emails`) { + const person = this.bearer(request); + if (person === undefined) return answer(request, 401, { message: 'Bad credentials' }); + return answer(request, 200, person.emails); + } + return answer(request, 404, { message: 'Not Found' }); + }; + + private token(request: AuthMethodEgressRequest): AuthMethodEgressResponse { + const form = new URLSearchParams(typeof request.body === 'string' ? request.body : ''); + if ( + form.get('client_id') !== this.config.clientId || + form.get('client_secret') !== this.config.clientSecret + ) { + return answer(request, 200, { error: 'incorrect_client_credentials' }); + } + const code = form.get('code') ?? ''; + const issued = this.codes.get(code); + // A code is good once. + this.codes.delete(code); + if (issued === undefined || form.get('redirect_uri') !== issued.redirectUri) { + return answer(request, 200, { error: 'bad_verification_code' }); + } + if (issued.challenge !== undefined) { + const verifier = form.get('code_verifier') ?? ''; + if (issued.challengeMethod !== 'S256' || s256(verifier) !== issued.challenge) { + return answer(request, 200, { error: 'bad_verification_code' }); + } + } + const token = `gho_${randomBytes(18).toString('base64url')}`; + this.tokens.push(token); + // The person as they are now, so a rename between sign-ins shows. + this.people.set(token, this.person); + return answer(request, 200, { access_token: token, token_type: 'bearer', scope: 'user:email' }); + } + + private bearer(request: AuthMethodEgressRequest): FakePerson | undefined { + const header = request.headers?.['authorization'] ?? ''; + const token = header.startsWith('Bearer ') ? header.slice('Bearer '.length) : ''; + return this.people.get(token); + } + + /** The browser back at the callback, signed in. */ + callback(authorizationRequest: URL): AuthMethodKitRequest { + const code = this.authorize(authorizationRequest); + const state = authorizationRequest.searchParams.get('state') ?? ''; + return { path: '/callback', method: 'GET', query: { code, state } }; + } + + /** The browser back at the callback, the person having declined. */ + declined(authorizationRequest: URL): AuthMethodKitRequest { + const state = authorizationRequest.searchParams.get('state') ?? ''; + return { + path: '/callback', + method: 'GET', + query: { + error: 'access_denied', + error_description: 'The user has denied your application access.', + state, + }, + }; + } + + /** The contract test kit's scenario, played by this fake. */ + scenario(): AuthMethodKitScenario { + return { + config: this.config, + egress: this.answer, + callback: (request) => this.callback(request), + declined: (request) => this.declined(request), + }; + } +} diff --git a/test/method.test.ts b/test/method.test.ts new file mode 100644 index 0000000..3c9cac9 --- /dev/null +++ b/test/method.test.ts @@ -0,0 +1,406 @@ +import { createHash } from 'node:crypto'; + +import type { + AuthMethodCallbackRoute, + AuthMethodEgressRequest, + AuthMethodParameters, + UpstreamCallbackOutcome, + UserAttribute, + VerifiedIdentity, +} from '@qauth-labs/plugin-sdk'; +import { assertAuthMethod } from '@qauth-labs/plugin-sdk'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { configSchema, type GitHubConfig } from '../src/config.js'; +import { authMethod, type GitHubTransaction } from '../src/index.js'; +import { CONFIG, FakeGitHub } from './fake-github.js'; +import { type FakeContext, fakeContext, REALM_ORIGIN } from './fake-context.js'; + +const callback = authMethod.routes[0] as AuthMethodCallbackRoute; + +/** One sign-in, from `start()` to the callback's outcome. */ +async function callbackOutcome( + fake: FakeGitHub, + context: FakeContext, + query?: (authorizationRequest: URL, code: string) => AuthMethodParameters +): Promise { + const start = await authMethod.start({ uiLocales: [] }, context); + const authorizationRequest = new URL(start.redirectTo); + const code = fake.authorize(authorizationRequest); + return callback.handle( + { + method: 'GET', + query: query?.(authorizationRequest, code) ?? { + code, + state: authorizationRequest.searchParams.get('state') ?? '', + }, + form: {}, + transaction: start.transaction, + }, + context + ); +} + +/** One whole sign-in: the identity and the attributes the server gets. */ +async function signIn( + fake: FakeGitHub, + context: FakeContext +): Promise<{ identity: VerifiedIdentity; attributes: UserAttribute[] }> { + const outcome = await callbackOutcome(fake, context); + if (outcome.kind !== 'result') throw new Error(`no result: ${JSON.stringify(outcome)}`); + const identity = await authMethod.verify(outcome.result); + return { identity, attributes: authMethod.extractAttributes(identity) }; +} + +function attribute(attributes: UserAttribute[], key: string): UserAttribute | undefined { + return attributes.find((entry) => entry.attrKey === key); +} + +describe('the GitHub AuthMethod', () => { + let fake: FakeGitHub; + let context: FakeContext; + + beforeEach(() => { + fake = new FakeGitHub(); + context = fakeContext(CONFIG, fake.answer); + }); + + it('meets the static checks of the contract', () => { + expect(() => { + assertAuthMethod(authMethod); + }).not.toThrow(); + expect(authMethod.type).toBe('github'); + expect(authMethod.id).toBe('github'); + expect(authMethod.routes.map((route) => `${route.method} ${route.path}`)).toEqual([ + 'GET /callback', + ]); + }); + + describe('start()', () => { + it('sends the browser to GitHub with state, PKCE S256, the user:email scope and its own callback', async () => { + const start = await authMethod.start({ uiLocales: ['tr'] }, context); + const url = new URL(start.redirectTo); + expect(`${url.origin}${url.pathname}`).toBe('https://github.com/login/oauth/authorize'); + expect(url.searchParams.get('client_id')).toBe(CONFIG.clientId); + expect(url.searchParams.get('redirect_uri')).toBe(`${REALM_ORIGIN}/upstream/github/callback`); + expect(url.searchParams.get('scope')).toBe('user:email'); + expect(url.searchParams.get('state')).toBe(start.transaction.state); + expect(start.binding).toBe(start.transaction.state); + expect(url.searchParams.get('code_challenge_method')).toBe('S256'); + expect(url.searchParams.get('code_challenge')).toBe( + createHash('sha256').update(start.transaction.codeVerifier, 'ascii').digest('base64url') + ); + // No call before the browser comes back. + expect(fake.calls).toEqual([]); + }); + + it('makes a fresh state and verifier for each sign-in', async () => { + const first = await authMethod.start({ uiLocales: [] }, context); + const second = await authMethod.start({ uiLocales: [] }, context); + expect(first.transaction.state).not.toBe(second.transaction.state); + expect(first.transaction.codeVerifier).not.toBe(second.transaction.codeVerifier); + expect(first.transaction.state).toMatch(/^[A-Za-z0-9_-]{43}$/); + expect(first.transaction.codeVerifier).toMatch(/^[A-Za-z0-9_-]{43}$/); + }); + + it('sends the browser to a GitHub Enterprise Server at its base URL', async () => { + const config = { ...CONFIG, baseUrl: 'https://github.corp.example/' }; + const start = await authMethod.start({ uiLocales: [] }, fakeContext(config, fake.answer)); + const url = new URL(start.redirectTo); + expect(`${url.origin}${url.pathname}`).toBe( + 'https://github.corp.example/login/oauth/authorize' + ); + }); + }); + + describe('the callback', () => { + it('redeems the code with the verifier, reads the person, and keys the identity by the numeric id', async () => { + const { identity, attributes } = await signIn(fake, context); + expect(identity.externalSub).toBe('1234567'); + expect(identity.assuranceLevel).toBe('low'); + expect(identity.rawClaims).toEqual({ + iss: 'https://github.com', + sub: '1234567', + login: 'octo-person', + name: 'Octo Person', + email: 'octo@example.com', + email_verified: true, + }); + expect(attributes).toEqual([ + { source: 'github', attrKey: 'email', attrValue: 'octo@example.com', verified: true }, + { + source: 'github', + attrKey: 'preferred_username', + attrValue: 'octo-person', + verified: false, + }, + { source: 'github', attrKey: 'name', attrValue: 'Octo Person', verified: false }, + ]); + }); + + it('asks the token endpoint for JSON, sends the secret only there, and follows no redirect with a credential', async () => { + await signIn(fake, context); + const [token, ...reads] = fake.calls; + expect(token).toMatchObject({ + method: 'POST', + url: 'https://github.com/login/oauth/access_token', + purpose: 'upstream-token', + source: 'operator', + redirect: 'error', + }); + expect(token?.headers?.['accept']).toBe('application/json'); + const form = new URLSearchParams(token?.body as string); + expect(form.get('grant_type')).toBe('authorization_code'); + expect(form.get('redirect_uri')).toBe(`${REALM_ORIGIN}/upstream/github/callback`); + expect(form.get('client_secret')).toBe(CONFIG.clientSecret); + expect(form.get('code_verifier')).toMatch(/^[A-Za-z0-9_-]{43}$/); + + expect(reads.map((call) => new URL(call.url).pathname).sort()).toEqual([ + '/user', + '/user/emails', + ]); + for (const read of reads) { + expect(read).toMatchObject({ method: 'GET', redirect: 'error', source: 'operator' }); + expect(read.url.startsWith('https://api.github.com/')).toBe(true); + expect(read.headers?.['authorization']).toBe(`Bearer ${fake.tokens[0] ?? ''}`); + expect(read.headers?.['user-agent']).toBe('qauth-labs-plugin-github'); + expect(JSON.stringify(read)).not.toContain(CONFIG.clientSecret); + } + }); + + it('signs a renamed login in as the same person', async () => { + const before = await signIn(fake, context); + fake.person.login = 'renamed-person'; + const after = await signIn(fake, context); + expect(after.identity.externalSub).toBe(before.identity.externalSub); + expect(attribute(after.attributes, 'preferred_username')?.attrValue).toBe('renamed-person'); + }); + + it('signs a changed email address in as the same person', async () => { + const before = await signIn(fake, context); + fake.person.emails = [{ email: 'new@example.org', primary: true, verified: true }]; + const after = await signIn(fake, context); + expect(after.identity.externalSub).toBe(before.identity.externalSub); + expect(attribute(after.attributes, 'email')?.attrValue).toBe('new@example.org'); + }); + + it('marks the email verified only for the primary address GitHub verified', async () => { + fake.person.emails = [ + { email: 'primary@example.com', primary: true, verified: false }, + { email: 'other@example.com', primary: false, verified: true }, + ]; + const unverified = await signIn(fake, context); + expect(attribute(unverified.attributes, 'email')).toEqual({ + source: 'github', + attrKey: 'email', + attrValue: 'primary@example.com', + verified: false, + }); + + fake.person.emails = [{ email: 'other@example.com', primary: false, verified: true }]; + const noPrimary = await signIn(fake, context); + expect(attribute(noPrimary.attributes, 'email')).toBeUndefined(); + expect(noPrimary.identity.rawClaims).not.toHaveProperty('email'); + }); + + it('takes no address when GitHub lists two primary ones', async () => { + fake.person.emails = [ + { email: 'a@example.com', primary: true, verified: true }, + { email: 'b@example.com', primary: true, verified: true }, + ]; + const { attributes } = await signIn(fake, context); + expect(attribute(attributes, 'email')).toBeUndefined(); + }); + + it('leaves out a display name the person never set', async () => { + fake.person.name = null; + const { identity, attributes } = await signIn(fake, context); + expect(identity.rawClaims).not.toHaveProperty('name'); + expect(attribute(attributes, 'name')).toBeUndefined(); + }); + + it('never hands the access token to the server or a log line', async () => { + const outcome = await callbackOutcome(fake, context); + expect(outcome.kind).toBe('result'); + const token = fake.tokens[0] ?? 'missing'; + expect(JSON.stringify(outcome)).not.toContain(token); + const { identity, attributes } = await signIn(fake, context); + expect(JSON.stringify({ identity, attributes })).not.toContain(fake.tokens[1] ?? 'missing'); + expect(JSON.stringify(context.lines)).not.toContain(token); + }); + + it('ends as declined when the person declines at GitHub', async () => { + const outcome = await callbackOutcome(fake, context, (request) => ({ + error: 'access_denied', + state: request.searchParams.get('state') ?? '', + })); + expect(outcome).toEqual({ kind: 'declined' }); + expect(fake.calls).toEqual([]); + }); + + it('fails with provider_error for another error GitHub sends back', async () => { + const outcome = await callbackOutcome(fake, context, (request) => ({ + error: 'redirect_uri_mismatch', + state: request.searchParams.get('state') ?? '', + })); + expect(outcome).toEqual({ kind: 'failed', reason: 'provider_error' }); + }); + + it('refuses a callback whose state is not the transaction’s', async () => { + const outcome = await callbackOutcome(fake, context, (_request, code) => ({ + code, + state: 'A'.repeat(43), + })); + expect(outcome).toEqual({ kind: 'failed', reason: 'transaction_mismatch' }); + expect(fake.calls).toEqual([]); + }); + + it('refuses a callback without a code, or with a repeated one', async () => { + const missing = await callbackOutcome(fake, context, (request) => ({ + state: request.searchParams.get('state') ?? '', + })); + expect(missing).toEqual({ kind: 'failed', reason: 'invalid_response' }); + const repeated = await callbackOutcome(fake, context, (request, code) => ({ + code: [code, code], + state: request.searchParams.get('state') ?? '', + })); + expect(repeated).toEqual({ kind: 'failed', reason: 'invalid_response' }); + }); + + it('fails with provider_error when GitHub refuses the code (200 with an error)', async () => { + const outcome = await callbackOutcome(fake, context, (request) => ({ + code: 'not-a-code-github-issued', + state: request.searchParams.get('state') ?? '', + })); + expect(outcome).toEqual({ kind: 'failed', reason: 'provider_error' }); + expect(context.lines.some((line) => line.fields['error'] === 'bad_verification_code')).toBe( + true + ); + }); + + it('fails with upstream_unavailable when GitHub cannot be reached or answers 5xx', async () => { + const refusing = fakeContext(CONFIG, (request: AuthMethodEgressRequest) => { + if (request.purpose === 'upstream-token') throw new Error('private address refused'); + return fake.answer(request); + }); + expect(await callbackOutcome(fake, refusing)).toEqual({ + kind: 'failed', + reason: 'upstream_unavailable', + }); + + fake.overrides.set('/login/oauth/access_token', (request) => ({ + status: 502, + headers: {}, + body: new Uint8Array(), + url: request.url, + })); + expect(await callbackOutcome(fake, context)).toEqual({ + kind: 'failed', + reason: 'upstream_unavailable', + }); + }); + + it('fails with invalid_response for an answer that is not GitHub’s', async () => { + fake.overrides.set('/user', (request) => ({ + status: 200, + headers: {}, + body: new TextEncoder().encode(JSON.stringify({ id: '1234567', login: 'octo-person' })), + url: request.url, + })); + expect(await callbackOutcome(fake, context)).toEqual({ + kind: 'failed', + reason: 'invalid_response', + }); + }); + + it('fails with invalid_response when the emails endpoint is refused (no user:email)', async () => { + fake.overrides.set('/user/emails', (request) => ({ + status: 403, + headers: {}, + body: new TextEncoder().encode( + JSON.stringify({ message: 'Resource not accessible by integration' }) + ), + url: request.url, + })); + expect(await callbackOutcome(fake, context)).toEqual({ + kind: 'failed', + reason: 'invalid_response', + }); + }); + }); + + describe('GitHub Enterprise Server', () => { + it('redeems at the base URL, reads its API under /api/v3, and names it as the issuer', async () => { + const config = { ...CONFIG, baseUrl: 'https://github.corp.example:8443' }; + const enterprise = new FakeGitHub(config); + const { identity } = await signIn(enterprise, fakeContext(config, enterprise.answer)); + expect(identity.rawClaims['iss']).toBe('https://github.corp.example:8443'); + expect( + enterprise.calls.map((call) => `${call.method} ${call.url} ${call.source}`).sort() + ).toEqual([ + 'GET https://github.corp.example:8443/api/v3/user admin', + 'GET https://github.corp.example:8443/api/v3/user/emails?per_page=100 admin', + 'POST https://github.corp.example:8443/login/oauth/access_token admin', + ]); + }); + + it('treats a base URL of github.com as github.com', async () => { + const config = { ...CONFIG, baseUrl: 'https://github.com' }; + const github = new FakeGitHub(config); + const { identity } = await signIn(github, fakeContext(config, github.answer)); + expect(identity.rawClaims['iss']).toBe('https://github.com'); + expect(github.calls.some((call) => call.url.startsWith('https://api.github.com/user'))).toBe( + true + ); + }); + }); + + describe('verify()', () => { + it('takes only a result of its own callback, once', async () => { + const outcome = await callbackOutcome(fake, context); + if (outcome.kind !== 'result') throw new Error('no result'); + await expect(authMethod.verify({ ...(outcome.result as object) })).rejects.toThrow(); + await expect( + authMethod.verify({ externalSub: '1', assuranceLevel: 'high', rawClaims: {} }) + ).rejects.toThrow(); + await expect(authMethod.verify(outcome.result)).resolves.toMatchObject({ + externalSub: '1234567', + }); + await expect(authMethod.verify(outcome.result)).rejects.toThrow(); + }); + }); + + describe('the config', () => { + it('takes a client id and secret, and an https base URL with nothing past the host and port', () => { + expect(configSchema.safeParse(CONFIG).success).toBe(true); + for (const baseUrl of [ + 'https://github.corp.example', + 'https://github.corp.example/', + 'https://10.1.2.3:8443', + ]) { + expect(configSchema.safeParse({ ...CONFIG, baseUrl }).success, baseUrl).toBe(true); + } + for (const baseUrl of [ + 'http://github.corp.example', + 'https://github.corp.example/api/v3', + 'https://user:pass@github.corp.example', + 'https://github.corp.example?x=1', + 'https://github.corp.example#top', + 'github.corp.example', + ]) { + expect(configSchema.safeParse({ ...CONFIG, baseUrl }).success, baseUrl).toBe(false); + } + expect(configSchema.safeParse({ ...CONFIG, clientSecret: '' }).success).toBe(false); + expect(configSchema.safeParse({ ...CONFIG, clientId: 'has space' }).success).toBe(false); + }); + + it('marks the client secret write-only', () => { + const schema = configSchema.toJSONSchema() as { + properties: Record; + }; + expect(schema.properties['clientSecret']?.writeOnly).toBe(true); + expect(schema.properties['clientId']?.writeOnly).toBeUndefined(); + expect(schema.properties['baseUrl']?.writeOnly).toBeUndefined(); + }); + }); +}); From cda3d3df91f9846a4b6fda78fc0d767b597c9695 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:36:53 +0100 Subject: [PATCH 04/10] test(e2e): the released server image with the plugin, against a mock GitHub e2e/run.sh packs the plugin, adds it to QAuth's released auth-server image the way an operator does, and starts it with PostgreSQL, Redis and a mock GitHub Enterprise Server on a private network, reachable under one name the egress allowlist lists and one it does not. The suite configures the plugin through the admin API and signs in through the server's pages. It checks the account keyed by the numeric id, renames and email changes, the verified-email rule, a wrong state, the allowlist, the boot with the plugin API off, and that no access token reaches the database, the events or the logs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- e2e/Dockerfile | 20 ++ e2e/compose.yml | 138 ++++++++++++ e2e/egress-allowlist.json | 9 + e2e/github.e2e.test.ts | 417 +++++++++++++++++++++++++++++++++++ e2e/harness.ts | 429 +++++++++++++++++++++++++++++++++++++ e2e/mock-github/server.mjs | 199 +++++++++++++++++ e2e/run.sh | 92 ++++++++ e2e/vitest.config.ts | 15 ++ 8 files changed, 1319 insertions(+) create mode 100644 e2e/Dockerfile create mode 100644 e2e/compose.yml create mode 100644 e2e/egress-allowlist.json create mode 100644 e2e/github.e2e.test.ts create mode 100644 e2e/harness.ts create mode 100644 e2e/mock-github/server.mjs create mode 100644 e2e/run.sh create mode 100644 e2e/vitest.config.ts diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 0000000..28203be --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,20 @@ +# The released QAuth server image with this plugin added the way an operator +# adds it (QAuth's docs: operate/images, "Add a plugin to the image"). The +# plugin is the tarball `npm pack` made of this checkout, the same file npm +# would publish; run.sh puts it at .work/plugin.tgz. +# +# The plugin is installed nested, so it brings its own copy of the plugin SDK, +# without its peer dependencies, so it uses the server's zod, and without +# install scripts. +ARG QAUTH_IMAGE=ghcr.io/qauth-labs/qauth/auth-server:0.2.0-rc.1@sha256:a4b199c5b1e896346d09c348f0a6cb055ab4f64bf82e0654740b0aa2631e5e8c + +FROM node:26-alpine@sha256:c6ad9e9c1d80f6eb607c26b4b0a132ff7a2344fad1c78ea3baa2a7d4c8de6d94 AS plugin +WORKDIR /plugin +COPY .work/plugin.tgz /tmp/plugin.tgz +RUN npm init --yes > /dev/null \ + && npm install --omit=dev --omit=peer --install-strategy=nested --ignore-scripts /tmp/plugin.tgz + +FROM ${QAUTH_IMAGE} +COPY --from=plugin --chown=nodejs:nodejs \ + /plugin/node_modules/@qauth-labs/plugin-github \ + /app/node_modules/@qauth-labs/plugin-github diff --git a/e2e/compose.yml b/e2e/compose.yml new file mode 100644 index 0000000..b773b77 --- /dev/null +++ b/e2e/compose.yml @@ -0,0 +1,138 @@ +# The end-to-end suite's deployment (run.sh starts it): QAuth's released +# images, the server with this plugin added (Dockerfile), and a mock GitHub +# Enterprise Server on the deployment's private network. +# +# The mock answers to two names. `mock-github` is on the operator's egress +# allowlist (egress-allowlist.json) and `mock-github-unlisted` is not, so the +# suite can show that a private base URL works only where the allowlist names +# it. The server's egress client resolves names through DNS, so both are +# network aliases. +# +# The secrets (JWT keys, KEK, session cookie secret, the mock's client +# secret) come from run.sh's environment, made fresh for each run. +name: plugin-github-e2e + +x-server: &server + build: + context: . + dockerfile: Dockerfile + image: plugin-github-e2e/auth-server:local + environment: &server-env + NODE_ENV: production + HOST: 0.0.0.0 + PORT: '3000' + LOG_LEVEL: info + DATABASE_URL: postgres://qauth:qauth@postgres:5432/qauth + REDIS_URL: redis://redis:6379/0 + REDIS_HOST: redis + REDIS_PORT: '6379' + REDIS_DB: '0' + JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY:?run e2e/run.sh} + JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY:?run e2e/run.sh} + JWT_ISSUER: http://localhost:3000 + KEK: ${KEK:?run e2e/run.sh} + SESSION_COOKIE_SECRET: ${SESSION_COOKIE_SECRET:?run e2e/run.sh} + # The suite talks to the server over plain http on localhost. + SESSION_COOKIE_SECURE: 'false' + # The first boot seeds the master realm with sign-up allowed, so a first + # sign-in through GitHub creates the account. + ALLOW_SIGN_UP: 'true' + # The suite signs in a dozen times from one address. + RATE_LIMIT_MAX: '10000' + LOGIN_RATE_LIMIT: '1000' + AUTHORIZE_RATE_LIMIT: '1000' + TOKEN_RATE_LIMIT: '1000' + REGISTER_CLIENT_RATE_LIMIT: '100' + # The plugin, as an operator installs it. + AUTH_METHOD_PLUGINS: '@qauth-labs/plugin-github' + PLUGIN_API_ENABLED: 'true' + EGRESS_ALLOWLIST_PATH: /etc/qauth/egress-allowlist.json + # The mock's certificate is signed by run.sh's throwaway CA. + NODE_EXTRA_CA_CERTS: /etc/qauth/e2e-ca.crt + # The admin API configures the plugin for the realm. Its base URL is only + # what DPoP proofs name (htu); the suite reaches it on port 3002. + ADMIN_API_ENABLED: 'true' + ADMIN_API_HOST: 0.0.0.0 + ADMIN_API_PORT: '3002' + ADMIN_API_BASE_URL: https://admin.qauth.test + DPOP_ENABLED: 'true' + volumes: + - ./egress-allowlist.json:/etc/qauth/egress-allowlist.json:ro + - ./.work/certs/ca.crt:/etc/qauth/e2e-ca.crt:ro + +services: + postgres: + image: postgres:18-alpine + environment: + POSTGRES_DB: qauth + POSTGRES_USER: qauth + POSTGRES_PASSWORD: qauth + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U qauth -d qauth'] + interval: 2s + timeout: 5s + retries: 30 + + redis: + image: redis:7-alpine + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 2s + timeout: 5s + retries: 30 + + migrate: + image: ghcr.io/qauth-labs/qauth/migration-runner:0.2.0-rc.1@sha256:d4c47545e04b1fd78f16146ffd8e20f02983722d20a71ffbfde4c4c8c95599f6 + environment: + DATABASE_URL: postgres://qauth:qauth@postgres:5432/qauth + depends_on: + postgres: + condition: service_healthy + + mock-github: + image: node:26-alpine@sha256:c6ad9e9c1d80f6eb607c26b4b0a132ff7a2344fad1c78ea3baa2a7d4c8de6d94 + command: ['node', '/mock/server.mjs'] + environment: + MOCK_GITHUB_CLIENT_ID: ${MOCK_GITHUB_CLIENT_ID:?run e2e/run.sh} + MOCK_GITHUB_CLIENT_SECRET: ${MOCK_GITHUB_CLIENT_SECRET:?run e2e/run.sh} + volumes: + - ./mock-github:/mock:ro + - ./.work/certs:/certs:ro + networks: + default: + aliases: [mock-github-unlisted] + # The suite plays the browser at the mock's authorization page, and reads + # its control API, from the host. + ports: + - 127.0.0.1:8443:8443 + + auth-server: + <<: *server + depends_on: + migrate: + condition: service_completed_successfully + redis: + condition: service_healthy + mock-github: + condition: service_started + ports: + - 127.0.0.1:3000:3000 + - 127.0.0.1:3002:3002 + + # The same image and plugin with the experimental plugin API off: the boot + # must skip the plugin and say why. run.sh starts it after the first server + # has seeded the database. + auth-server-plugin-api-off: + <<: *server + environment: + <<: *server-env + PLUGIN_API_ENABLED: 'false' + ADMIN_API_ENABLED: 'false' + depends_on: + migrate: + condition: service_completed_successfully + redis: + condition: service_healthy + ports: + - 127.0.0.1:3010:3000 + profiles: [plugin-api-off] diff --git a/e2e/egress-allowlist.json b/e2e/egress-allowlist.json new file mode 100644 index 0000000..a3b486c --- /dev/null +++ b/e2e/egress-allowlist.json @@ -0,0 +1,9 @@ +{ + "entries": [ + { + "host": "mock-github", + "realms": ["master"], + "note": "The mock GitHub Enterprise Server. Its other name, mock-github-unlisted, is not here." + } + ] +} diff --git a/e2e/github.e2e.test.ts b/e2e/github.e2e.test.ts new file mode 100644 index 0000000..3840f4a --- /dev/null +++ b/e2e/github.e2e.test.ts @@ -0,0 +1,417 @@ +/** + * The plugin end to end, where an operator runs it: QAuth's released server + * image with the plugin installed on top (e2e/Dockerfile), against a mock + * GitHub Enterprise Server on the deployment's private network. The suite + * configures the plugin through the admin API, as an operator does, and signs + * in through the server's own pages, as a browser does. + * + * Run it through e2e/run.sh. + */ +import { createHash, randomBytes } from 'node:crypto'; + +import { beforeAll, describe, expect, it } from 'vitest'; + +import { + Admin, + browse, + CookieJar, + databaseDump, + formsOf, + issuedTokens, + linkWithClass, + literal, + logsOf, + MOCK, + mockCalls, + mockClient, + type MockPerson, + setPerson, + sql, +} from './harness.js'; + +/** Where the test application wants the code; nothing listens there. */ +const REDIRECT_URI = 'http://127.0.0.1:9/plugin-github-e2e/callback'; +/** The callback this provider alone uses. */ +const CALLBACK = 'http://localhost:3000/upstream/github/callback'; +/** The mock GitHub under the name the operator's allowlist names. */ +const LISTED_BASE_URL = 'https://mock-github:8443'; +/** The same mock under a name the allowlist does not name. */ +const UNLISTED_BASE_URL = 'https://mock-github-unlisted:8443'; + +const SIGN_IN_FAILED = 'urn:qauth:event:user.sign_in_failed:v1-rc.0'; +const REGISTERED = 'urn:qauth:event:user.registered:v1-rc.0'; + +let admin: Admin; +let clientId: string; +let realmId: string; + +function isRedirect(response: Response): boolean { + return response.status === 302 || response.status === 303; +} + +/** Configure the plugin for the realm and enable it, through the admin API. */ +async function configure(baseUrl: string): Promise { + const { clientId: githubClientId, clientSecret } = mockClient(); + const current = await admin.call('GET', '/plugins/github'); + expect(current.status).toBe(200); + const sealed = await admin.call('POST', '/plugins/github/secrets/seal', { + path: 'clientSecret', + value: clientSecret, + }); + expect(sealed.status).toBe(200); + const body = { + version: current.body['version'], + config: { clientId: githubClientId, baseUrl }, + secrets: { clientSecret: { sealed: sealed.body['sealed'] } }, + }; + const configured = await admin.call('POST', '/plugins/github/config', body, { + 'qauth-approval': await admin.approved('plugins.update', body), + }); + expect(configured.status, JSON.stringify(configured.body)).toBe(200); + if (configured.body['enabled'] !== true) { + const enableBody = { version: configured.body['version'] }; + const enabled = await admin.call('POST', '/plugins/github/enable', enableBody, { + 'qauth-approval': await admin.approved('plugins.enable', enableBody), + }); + expect(enabled.status, JSON.stringify(enabled.body)).toBe(200); + expect(enabled.body['enabled']).toBe(true); + } +} + +/** A sign-in started at the server and sent on to GitHub. */ +interface StartedSignIn { + readonly jar: CookieJar; + /** GitHub's authorization page, as the server sent the browser there. */ + readonly providerUrl: URL; +} + +/** An application asks the server to sign someone in through GitHub. */ +async function startSignIn(): Promise { + const jar = new CookieJar(); + const verifier = randomBytes(32).toString('base64url'); + const authorize = new URL('/oauth/authorize', 'http://localhost:3000'); + authorize.search = new URLSearchParams({ + response_type: 'code', + client_id: clientId, + redirect_uri: REDIRECT_URI, + scope: 'openid', + state: randomBytes(16).toString('base64url'), + code_challenge: createHash('sha256').update(verifier).digest('base64url'), + code_challenge_method: 'S256', + qauth_upstream: 'github', + }).toString(); + const toStart = await browse(jar, authorize); + expect(isRedirect(toStart)).toBe(true); + const startUrl = toStart.headers.get('location') ?? ''; + expect(startUrl).toMatch(/^\/ui\/upstream\/github\/start\?/); + const start = await browse(jar, startUrl); + expect(start.status).toBe(200); + const href = linkWithClass(await start.text(), 'alt-action'); + expect(href).toBeDefined(); + return { jar, providerUrl: new URL(href ?? '') }; +} + +/** + * The person signs in at the mock's authorization page; the URL GitHub sends + * the browser back to. The suite reaches the mock from the host as localhost. + */ +async function atGitHub(providerUrl: URL): Promise { + const page = new URL(providerUrl.pathname + providerUrl.search, MOCK); + const answer = await fetch(page, { redirect: 'manual' }); + expect(isRedirect(answer)).toBe(true); + const back = new URL(answer.headers.get('location') ?? ''); + expect(`${back.origin}${back.pathname}`).toBe(CALLBACK); + return back; +} + +/** + * Follow the server from the callback to the application, granting consent + * on the way when the page asks; the code the application receives. + */ +async function toApplication(jar: CookieJar, afterCallback: Response): Promise { + let response = afterCallback; + for (let step = 0; step < 10; step += 1) { + const location = response.headers.get('location'); + if (isRedirect(response) && location !== null) { + const next = new URL(location, 'http://localhost:3000'); + if (next.href.startsWith(REDIRECT_URI)) { + const code = next.searchParams.get('code'); + expect(next.searchParams.get('error')).toBeNull(); + expect(code).not.toBeNull(); + return code ?? ''; + } + response = await browse(jar, next); + continue; + } + expect(response.status, `step ${String(step)}`).toBe(200); + const consent = formsOf(await response.text()).find( + (form) => + form.action.endsWith('/ui/consent') || + form.buttons.some((button) => button.name === 'decision' && button.value === 'allow') + ); + expect(consent, 'a consent form').toBeDefined(); + if (consent === undefined) break; + const form = new URLSearchParams(consent.fields); + form.set('decision', 'allow'); + response = await browse(jar, consent.action, { method: 'POST', form }); + } + throw new Error('the server did not send the browser back to the application'); +} + +/** A whole sign-in of `person`; the code the application receives. */ +async function signIn(person: MockPerson): Promise { + await setPerson(person); + const { jar, providerUrl } = await startSignIn(); + const back = await atGitHub(providerUrl); + const callback = await browse(jar, back); + expect(isRedirect(callback), await callback.clone().text()).toBe(true); + return toApplication(jar, callback); +} + +/** The accounts the GitHub credential of `id` belongs to. */ +function accountsOf(id: number): string[] { + return sql( + `SELECT user_id FROM user_credentials + WHERE provider_type = 'github' AND external_sub = ${literal(String(id))}` + ); +} + +function userCount(): number { + return Number(sql('SELECT count(*) FROM users')[0]); +} + +/** The attributes the plugin wrote for an account: key, value and whether verified. */ +function attributesOf(accountId: string): { key: string; value: string; verified: boolean }[] { + return sql( + `SELECT attr_key, attr_value, verified FROM user_attributes + WHERE user_id = ${literal(accountId)} AND source = 'github' ORDER BY attr_key` + ).map((row) => { + const [key = '', value = '', verified = ''] = row.split('|'); + return { key, value, verified: verified === 't' }; + }); +} + +function failedSignIns(reason: string): number { + return Number( + sql( + `SELECT count(*) FROM events WHERE type = ${literal(SIGN_IN_FAILED)} + AND data->>'method' = 'upstream' AND data->>'reason' = ${literal(reason)}` + )[0] + ); +} + +const octo: MockPerson = { + id: 58_000_001, + login: 'octo-first', + name: 'Octo First', + emails: [ + { email: 'octo@example.com', primary: true, verified: true }, + { email: '58000001+octo-first@users.noreply.example.com', primary: false, verified: true }, + ], +}; + +beforeAll(async () => { + const [master] = sql(`SELECT id FROM realms WHERE name = 'master'`); + if (master === undefined) throw new Error('no master realm'); + realmId = master; + admin = await Admin.create(realmId); + const registered = await fetch('http://localhost:3000/oauth/register', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + client_name: 'plugin-github e2e application', + redirect_uris: [REDIRECT_URI], + grant_types: ['authorization_code'], + response_types: ['code'], + token_endpoint_auth_method: 'none', + scope: 'openid', + }), + }); + expect(registered.status).toBe(201); + clientId = ((await registered.json()) as { client_id: string }).client_id; +}); + +describe('the boot', () => { + it('loads the plugin as an operator-installed package while PLUGIN_API_ENABLED is on', () => { + expect(logsOf('auth-server')).toContain( + 'AuthMethod plugin @qauth-labs/plugin-github loaded: github (github)' + ); + }); + + it('skips the plugin with PLUGIN_API_ENABLED off, and says why', () => { + const log = logsOf('auth-server-plugin-api-off'); + expect(log).toContain( + 'The AuthMethod plugin @qauth-labs/plugin-github is not loaded: it is not a first-party plugin, and the experimental plugin API is off.' + ); + expect(log).not.toContain('AuthMethod plugin @qauth-labs/plugin-github loaded'); + }); + + it('lists the plugin in the admin API, not configured yet', async () => { + const plugin = await admin.call('GET', '/plugins/github'); + expect(plugin.status).toBe(200); + expect(plugin.body).toMatchObject({ + type: 'github', + package: '@qauth-labs/plugin-github', + firstParty: false, + contract: 'qauth-auth-method/1-rc.0', + enabled: false, + }); + }); +}); + +describe('a private base URL', () => { + it('is refused while the operator allowlist does not name it', async () => { + await configure(UNLISTED_BASE_URL); + await setPerson(octo); + const refusedBefore = failedSignIns('upstream_unavailable'); + const callsBefore = (await mockCalls()).length; + + const { jar, providerUrl } = await startSignIn(); + expect(providerUrl.origin).toBe(UNLISTED_BASE_URL); + const back = await atGitHub(providerUrl); + const callback = await browse(jar, back); + + expect(callback.status).toBe(400); + expect(await callback.text()).toContain('We could not sign you in'); + expect(failedSignIns('upstream_unavailable')).toBe(refusedBefore + 1); + // The egress client refused the address before anything was sent: the + // mock saw the browser at its authorization page, and nothing else. + const calls = (await mockCalls()).slice(callsBefore); + expect(calls.map((call) => call.path)).toEqual(['/login/oauth/authorize']); + expect(logsOf('auth-server')).toContain('egress call blocked'); + expect(accountsOf(octo.id)).toEqual([]); + }); + + it('works once it is the name the allowlist names', async () => { + await configure(LISTED_BASE_URL); + const { providerUrl } = await startSignIn(); + expect(providerUrl.origin).toBe(LISTED_BASE_URL); + }); +}); + +describe('sign-in', () => { + let account: string; + + it('the first sign-in creates an account keyed by the numeric id', async () => { + const before = userCount(); + await signIn(octo); + + const accounts = accountsOf(octo.id); + expect(accounts).toHaveLength(1); + account = accounts[0] ?? ''; + expect(userCount()).toBe(before + 1); + // The subject is the id, under the GitHub that issued it; never the login. + const [credential] = sql( + `SELECT external_sub, credential_data->>'iss' FROM user_credentials + WHERE user_id = ${literal(account)} AND provider_type = 'github'` + ); + expect(credential).toBe(`${String(octo.id)}|${LISTED_BASE_URL}`); + expect( + sql(`SELECT 1 FROM user_credentials WHERE external_sub = ${literal(octo.login)}`) + ).toEqual([]); + expect( + sql( + `SELECT data->>'method' FROM events + WHERE type = ${literal(REGISTERED)} AND subject_id = ${literal(account)}` + ) + ).toEqual(['upstream']); + }); + + it('stores the primary verified email as verified, the login and the name as unverified', () => { + expect(attributesOf(account)).toEqual([ + { key: 'email', value: 'octo@example.com', verified: true }, + { key: 'name', value: 'Octo First', verified: false }, + { key: 'preferred_username', value: 'octo-first', verified: false }, + ]); + }); + + it('a renamed login signs in the same account', async () => { + const before = userCount(); + await signIn({ ...octo, login: 'octo-renamed', name: 'Octo Renamed' }); + expect(accountsOf(octo.id)).toEqual([account]); + expect(userCount()).toBe(before); + }); + + it('a changed email address signs in the same account, and no new account is created', async () => { + const before = userCount(); + await signIn({ + ...octo, + emails: [{ email: 'octo-changed@example.com', primary: true, verified: true }], + }); + expect(accountsOf(octo.id)).toEqual([account]); + expect(userCount()).toBe(before); + }); + + it('only a primary and verified email becomes a verified attribute', async () => { + const unverified: MockPerson = { + id: 58_000_002, + login: 'octo-unverified', + name: null, + emails: [ + { email: 'primary-unverified@example.com', primary: true, verified: false }, + { email: 'other-verified@example.com', primary: false, verified: true }, + ], + }; + await signIn(unverified); + const [second] = accountsOf(unverified.id); + expect(second).toBeDefined(); + expect(second).not.toBe(account); + // The primary address, unverified; the verified address that is not + // primary is not taken at all. + expect(attributesOf(second ?? '')).toEqual([ + { key: 'email', value: 'primary-unverified@example.com', verified: false }, + { key: 'preferred_username', value: 'octo-unverified', verified: false }, + ]); + expect( + sql(`SELECT 1 FROM user_attributes WHERE attr_value = 'other-verified@example.com'`) + ).toEqual([]); + // No address of the first account is verified but the one it began with. + expect( + sql( + `SELECT attr_value FROM user_attributes + WHERE source = 'github' AND attr_key = 'email' AND verified` + ) + ).toEqual(['octo@example.com']); + }); + + it('a callback with a wrong state is refused', async () => { + await setPerson(octo); + const mismatchesBefore = failedSignIns('transaction_mismatch'); + const tokensBefore = (await issuedTokens()).length; + const { jar, providerUrl } = await startSignIn(); + const back = await atGitHub(providerUrl); + expect(back.searchParams.get('state')).toBe(providerUrl.searchParams.get('state')); + + const forged = new URL(back); + forged.searchParams.set('state', randomBytes(32).toString('base64url')); + const refused = await browse(jar, forged); + + expect(refused.status).toBe(400); + expect(await refused.text()).toContain('We could not sign you in'); + expect(failedSignIns('transaction_mismatch')).toBe(mismatchesBefore + 1); + // The code was never redeemed. + expect(await issuedTokens()).toHaveLength(tokensBefore); + }); +}); + +describe('the access token', () => { + it('is not in the database, the logs or the events', async () => { + const tokens = await issuedTokens(); + // One per completed sign-in above. + expect(tokens.length).toBeGreaterThanOrEqual(4); + + const dump = databaseDump(); + const events = sql('SELECT row_to_json(events)::text FROM events').join('\n'); + const logs = [logsOf('auth-server'), logsOf('auth-server-plugin-api-off')].join('\n'); + expect(events).toContain(REGISTERED); + for (const token of tokens) { + expect(dump.includes(token), 'a token in the database').toBe(false); + expect(events.includes(token), 'a token in the events').toBe(false); + expect(logs.includes(token), 'a token in the logs').toBe(false); + } + // Nor is the client secret, which the server keeps sealed. + const { clientSecret } = mockClient(); + expect(dump.includes(clientSecret), 'the client secret in the database').toBe(false); + expect(logs.includes(clientSecret), 'the client secret in the logs').toBe(false); + }); +}); diff --git a/e2e/harness.ts b/e2e/harness.ts new file mode 100644 index 0000000..ccc8e9d --- /dev/null +++ b/e2e/harness.ts @@ -0,0 +1,429 @@ +/** + * What the end-to-end suite drives the deployment with: a browser of sorts (a + * cookie jar and the HTML forms of QAuth's pages), the admin API (an admin + * account, a DPoP-bound access token and a fresh DPoP proof for each call), + * the database and the logs (through docker compose), and the mock GitHub's + * control API. e2e/run.sh starts the deployment and sets the environment. + */ +import { execFileSync } from 'node:child_process'; +import { + createHash, + createPrivateKey, + generateKeyPairSync, + type JsonWebKey, + type KeyObject, + randomBytes, + randomUUID, + sign, +} from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +/** The server's public listener, as the suite reaches it. */ +export const SERVER = 'http://localhost:3000'; +/** The admin API's listener. */ +export const ADMIN = 'http://localhost:3002'; +/** What DPoP proofs for the admin API name: the server's ADMIN_API_BASE_URL. */ +const ADMIN_BASE_URL = 'https://admin.qauth.test'; +/** The mock GitHub, from the host. */ +export const MOCK = 'https://localhost:8443'; +/** The admin API's version prefix. */ +const ADMIN_PREFIX = '/admin/v1-rc.0'; + +function required(name: string): string { + const value = process.env[name]; + if (value === undefined || value === '') { + throw new Error(`${name} is not set: run the suite through e2e/run.sh`); + } + return value; +} + +/** Run `docker compose` on the suite's deployment; its standard output. */ +export function compose(...args: string[]): string { + return execFileSync('docker', ['compose', '--file', required('E2E_COMPOSE_FILE'), ...args], { + encoding: 'utf8', + maxBuffer: 256 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'], + }); +} + +/** Run one SQL statement in the server's database; its rows, one line each, columns split by `|`. */ +export function sql(statement: string): string[] { + const out = compose( + 'exec', + '-T', + 'postgres', + 'psql', + '--username=qauth', + '--dbname=qauth', + '--no-align', + '--tuples-only', + '--quiet', + '--set=ON_ERROR_STOP=1', + '--command', + statement + ); + return out.split('\n').filter((line) => line !== ''); +} + +/** A SQL string literal. */ +export function literal(value: string): string { + return `'${value.replaceAll("'", "''")}'`; +} + +/** Everything the server's database holds, as `pg_dump` writes it. */ +export function databaseDump(): string { + return compose( + 'exec', + '-T', + 'postgres', + 'pg_dump', + '--username=qauth', + '--dbname=qauth', + '--data-only' + ); +} + +/** A service's log, as docker keeps it. */ +export function logsOf(service: string): string { + return compose('logs', '--no-color', '--no-log-prefix', service); +} + +/** The client secret the mock GitHub expects, and the client id. */ +export function mockClient(): { clientId: string; clientSecret: string } { + return { + clientId: required('MOCK_GITHUB_CLIENT_ID'), + clientSecret: required('MOCK_GITHUB_CLIENT_SECRET'), + }; +} + +// --------------------------------------------------------------------------- +// The mock GitHub's control API. + +export interface MockEmail { + email: string; + primary: boolean; + verified: boolean; +} + +export interface MockPerson { + id: number; + login: string; + name: string | null; + emails: MockEmail[]; +} + +export interface MockCall { + method: string; + host: string; + path: string; +} + +async function mockControl(path: string, init?: RequestInit): Promise { + const response = await fetch(`${MOCK}/_control${path}`, init); + if (!response.ok) throw new Error(`mock control ${path}: ${String(response.status)}`); + return response.json(); +} + +/** Set the person who signs in at the mock's authorization page next. */ +export async function setPerson(person: MockPerson): Promise { + await mockControl('/person', { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(person), + }); +} + +/** Every access token the mock issued. */ +export async function issuedTokens(): Promise { + return ((await mockControl('/tokens')) as { tokens: string[] }).tokens; +} + +/** Every call the mock answered. */ +export async function mockCalls(): Promise { + return ((await mockControl('/calls')) as { calls: MockCall[] }).calls; +} + +// --------------------------------------------------------------------------- +// A browser of sorts. + +/** The cookies the server set, by name. Every request goes to one origin. */ +export class CookieJar { + private readonly cookies = new Map(); + + storeFrom(response: Response): void { + for (const header of response.headers.getSetCookie()) { + const pair = header.split(';', 1)[0] ?? ''; + const equals = pair.indexOf('='); + if (equals <= 0) continue; + const name = pair.slice(0, equals).trim(); + const value = pair.slice(equals + 1).trim(); + if (/;\s*max-age=0\b/i.test(header) || value === '') { + this.cookies.delete(name); + } else { + this.cookies.set(name, value); + } + } + } + + header(): string | undefined { + if (this.cookies.size === 0) return undefined; + return [...this.cookies].map(([name, value]) => `${name}=${value}`).join('; '); + } +} + +/** One request to the server with the jar's cookies; redirects are not followed. */ +export async function browse( + jar: CookieJar, + url: string | URL, + init: { method?: string; form?: URLSearchParams } = {} +): Promise { + const headers: Record = {}; + const cookie = jar.header(); + if (cookie !== undefined) headers['cookie'] = cookie; + let body: string | undefined; + if (init.form !== undefined) { + headers['content-type'] = 'application/x-www-form-urlencoded'; + body = init.form.toString(); + } + const response = await fetch(new URL(url, SERVER), { + method: init.method ?? 'GET', + headers, + redirect: 'manual', + ...(body === undefined ? {} : { body }), + }); + jar.storeFrom(response); + return response; +} + +const ENTITIES: Readonly> = { + amp: '&', + lt: '<', + gt: '>', + quot: '"', + apos: "'", +}; + +/** Decode the character references in an HTML attribute value. */ +export function decodeAttribute(value: string): string { + return value.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, reference: string) => { + if (reference.startsWith('#')) { + const code = /^#x/i.test(reference) + ? Number.parseInt(reference.slice(2), 16) + : Number.parseInt(reference.slice(1), 10); + return Number.isFinite(code) ? String.fromCodePoint(code) : match; + } + return ENTITIES[reference.toLowerCase()] ?? match; + }); +} + +function attributesOf(tag: string): Map { + const attributes = new Map(); + const body = tag.replace(/^<\s*[a-z]+/i, '').replace(/\/?>$/, ''); + for (const match of body.matchAll( + /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g + )) { + const name = match[1]; + if (name === undefined) continue; + attributes.set(name.toLowerCase(), decodeAttribute(match[2] ?? match[3] ?? match[4] ?? '')); + } + return attributes; +} + +/** The URL of the first `` with the given class. */ +export function linkWithClass(html: string, className: string): string | undefined { + for (const match of html.matchAll(/]*>/gi)) { + const attributes = attributesOf(match[0]); + const classes = (attributes.get('class') ?? '').split(/\s+/); + if (classes.includes(className)) return attributes.get('href'); + } + return undefined; +} + +/** A form as the page rendered it. */ +export interface HtmlForm { + readonly action: string; + readonly fields: URLSearchParams; + readonly buttons: ReadonlyArray<{ readonly name: string; readonly value: string }>; +} + +/** Every `
` of a page, with its inputs as a browser would send them. */ +export function formsOf(html: string): HtmlForm[] { + const forms: HtmlForm[] = []; + for (const match of html.matchAll(/]*>([\s\S]*?)<\/form>/gi)) { + const formTag = /^]*>/i.exec(match[0])?.[0] ?? ''; + const fields = new URLSearchParams(); + for (const input of (match[1] ?? '').matchAll(/]*>/gi)) { + const attributes = attributesOf(input[0]); + const name = attributes.get('name'); + if (name === undefined || name === '') continue; + const type = (attributes.get('type') ?? 'text').toLowerCase(); + if ((type === 'checkbox' || type === 'radio') && !attributes.has('checked')) continue; + if (type === 'submit' || type === 'button') continue; + fields.append(name, attributes.get('value') ?? (type === 'checkbox' ? 'on' : '')); + } + const buttons: { name: string; value: string }[] = []; + for (const button of (match[1] ?? '').matchAll(/]*>/gi)) { + const attributes = attributesOf(button[0]); + const name = attributes.get('name'); + if (name !== undefined && (attributes.get('type') ?? 'submit').toLowerCase() === 'submit') { + buttons.push({ name, value: attributes.get('value') ?? '' }); + } + } + forms.push({ action: attributesOf(formTag).get('action') ?? '', fields, buttons }); + } + return forms; +} + +// --------------------------------------------------------------------------- +// JOSE, with node:crypto alone. + +function base64url(value: string | Buffer): string { + return Buffer.from(value).toString('base64url'); +} + +function jws( + header: Record, + claims: Record, + signWith: (input: Buffer) => Buffer +): string { + const input = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(claims))}`; + return `${input}.${base64url(signWith(Buffer.from(input, 'ascii')))}`; +} + +/** An RFC 7638 thumbprint of a P-256 public key. */ +function thumbprint(jwk: JsonWebKey): string { + const members = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; + return base64url(createHash('sha256').update(JSON.stringify(members)).digest()); +} + +/** The admin API, as one security admin of the master realm. */ +export class Admin { + private constructor( + readonly realmId: string, + readonly accountId: string, + private readonly token: string, + private readonly proofKey: KeyObject, + private readonly proofJwk: JsonWebKey + ) {} + + /** + * Make a security admin for the realm (an account and a grant, written + * straight into the database, as the operator's bootstrap does), and an + * access token for it, bound to a fresh DPoP key and signed with the + * server's key, as the server issues one. + */ + static async create(realmId: string): Promise { + const [accountId] = sql( + `WITH account AS (INSERT INTO users (realm_id) VALUES (${literal(realmId)}) RETURNING id) + INSERT INTO admin_grants (user_id, built_in_role, target_realm_id) + SELECT id, 'security-admin', ${literal(realmId)} FROM account RETURNING user_id` + ); + if (accountId === undefined) throw new Error('no admin account'); + + const discovery = (await ( + await fetch(`${SERVER}/.well-known/openid-configuration`) + ).json()) as { issuer: string; jwks_uri: string }; + const signingKey = createPrivateKey( + readFileSync(join(required('E2E_WORK'), 'keys', 'jwt-private.pem')) + ); + const signingJwk = signingKey.export({ format: 'jwk' }); + const jwks = (await ( + await fetch(discovery.jwks_uri.replace(/^https?:\/\/[^/]+/, SERVER)) + ).json()) as { + keys: (JsonWebKey & { kid?: string })[]; + }; + const kid = jwks.keys.find((key) => key.x === signingJwk.x)?.kid; + if (kid === undefined) throw new Error('the server does not publish its signing key'); + + const { privateKey: proofKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const proofJwk = publicKey.export({ format: 'jwk' }); + const now = Math.floor(Date.now() / 1000); + const token = jws( + { alg: 'EdDSA', typ: 'at+jwt', kid }, + { + iss: discovery.issuer, + sub: accountId, + aud: 'urn:qauth:aud:admin:v1-rc.0', + client_id: 'plugin-github-e2e', + token_use: 'access', + scope: 'qauth:admin:read qauth:admin:write qauth:admin:security', + jti: randomUUID(), + iat: now, + exp: now + 1800, + cnf: { jkt: thumbprint(proofJwk) }, + }, + (input) => sign(null, input, signingKey) + ); + return new Admin(realmId, accountId, token, proofKey, proofJwk); + } + + /** A DPoP proof for one call (RFC 9449 §4.2). */ + private proof(method: string, path: string): string { + return jws( + { typ: 'dpop+jwt', alg: 'ES256', jwk: this.proofJwk }, + { + jti: randomBytes(16).toString('base64url'), + htm: method, + htu: `${ADMIN_BASE_URL}${path}`, + iat: Math.floor(Date.now() / 1000), + ath: base64url(createHash('sha256').update(this.token, 'ascii').digest()), + }, + (input) => sign('sha256', input, { key: this.proofKey, dsaEncoding: 'ieee-p1363' }) + ); + } + + /** One admin API call, below the realm; its status and JSON body. */ + async call( + method: 'GET' | 'POST', + path: string, + body?: unknown, + headers: Record = {} + ): Promise<{ status: number; body: Record }> { + const fullPath = `${ADMIN_PREFIX}/realms/${this.realmId}${path}`; + const response = await fetch(`${ADMIN}${fullPath}`, { + method, + headers: { + authorization: `DPoP ${this.token}`, + dpop: this.proof(method, fullPath), + accept: 'application/json', + ...(body === undefined ? {} : { 'content-type': 'application/json' }), + ...headers, + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + const text = await response.text(); + return { + status: response.status, + body: text === '' ? {} : (JSON.parse(text) as Record), + }; + } + + /** + * File an approval for a security operation and approve it. The approval + * is written as approved straight into the database: the suite has no + * passkey to approve it with. The server's gate checks it as it checks + * any other. + */ + async approved(operation: string, body: unknown): Promise { + const filed = await this.call('POST', '/approvals', { + operation, + params: { plugin: 'github' }, + body, + }); + if (filed.status !== 201 || typeof filed.body['id'] !== 'string') { + throw new Error( + `approval for ${operation}: ${String(filed.status)} ${JSON.stringify(filed.body)}` + ); + } + const id = filed.body['id']; + sql( + `UPDATE admin_approvals SET status = 'approved', approvals = jsonb_build_array(jsonb_build_object( + 'approverRealmId', requester_realm_id::text, 'approverId', requester_id::text, + 'credentialId', 'plugin-github-e2e', 'factor', 'webauthn', + 'approvedAt', (extract(epoch FROM clock_timestamp()) * 1000)::bigint)) + WHERE id = ${literal(id)}` + ); + return id; + } +} diff --git a/e2e/mock-github/server.mjs b/e2e/mock-github/server.mjs new file mode 100644 index 0000000..1470b1b --- /dev/null +++ b/e2e/mock-github/server.mjs @@ -0,0 +1,199 @@ +/** + * A mock GitHub Enterprise Server for the end-to-end suite, over https: the + * authorization page, the token endpoint, `GET /api/v3/user` and + * `GET /api/v3/user/emails`, answering as GitHub does. It checks what GitHub + * checks: the client, the code (good once), the `redirect_uri`, the PKCE + * verifier, the bearer token and the `User-Agent`. + * + * The test drives it through a control API under `/_control/`: it sets the + * person who signs in next, and reads back every access token the mock + * issued and every call it answered, so it can look for those tokens in the + * server's database and logs. + * + * No dependencies: it runs on the plain node image. + */ +import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { createServer } from 'node:https'; + +const PORT = Number(process.env.PORT ?? '8443'); +const CLIENT_ID = process.env.MOCK_GITHUB_CLIENT_ID ?? ''; +const CLIENT_SECRET = process.env.MOCK_GITHUB_CLIENT_SECRET ?? ''; +if (CLIENT_ID === '' || CLIENT_SECRET === '') { + throw new Error('MOCK_GITHUB_CLIENT_ID and MOCK_GITHUB_CLIENT_SECRET are required'); +} + +/** The person who signs in at the authorization page. The test replaces it. */ +let person = { + id: 1001, + login: 'first-person', + name: 'First Person', + emails: [{ email: 'first@example.com', primary: true, verified: true, visibility: 'private' }], +}; + +/** Issued codes: the request they answer, and the person as they were then. */ +const codes = new Map(); +/** Issued access tokens and the person each belongs to. */ +const people = new Map(); +/** Every access token issued, in order. */ +const tokens = []; +/** Every call answered, without its credentials: method, host and path. */ +const calls = []; + +function s256(verifier) { + return createHash('sha256').update(verifier, 'ascii').digest('base64url'); +} + +function same(left, right) { + const a = Buffer.from(String(left)); + const b = Buffer.from(String(right)); + return a.length === b.length && timingSafeEqual(a, b); +} + +function json(response, status, body) { + const text = JSON.stringify(body); + response.writeHead(status, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(text), + }); + response.end(text); +} + +async function bodyOf(request) { + const chunks = []; + let size = 0; + for await (const chunk of request) { + size += chunk.length; + if (size > 64 * 1024) throw new Error('body too large'); + chunks.push(chunk); + } + return Buffer.concat(chunks).toString('utf8'); +} + +function authorize(url, response) { + const query = url.searchParams; + const redirectUri = query.get('redirect_uri'); + const state = query.get('state'); + if (query.get('client_id') !== CLIENT_ID || redirectUri === null || state === null) { + return json(response, 400, { message: 'not a request of the registered app' }); + } + const code = randomBytes(10).toString('hex'); + codes.set(code, { + redirectUri, + challenge: query.get('code_challenge'), + challengeMethod: query.get('code_challenge_method'), + person: structuredClone(person), + }); + const back = new URL(redirectUri); + back.searchParams.set('code', code); + back.searchParams.set('state', state); + response.writeHead(302, { location: back.href }); + response.end(); +} + +function token(request, form, response) { + // GitHub answers JSON only when asked; the plugin asks. + if (!(request.headers.accept ?? '').includes('application/json')) { + response.writeHead(200, { 'content-type': 'application/x-www-form-urlencoded' }); + return response.end('error=not_asked_for_json'); + } + if ( + !same(form.get('client_id') ?? '', CLIENT_ID) || + !same(form.get('client_secret') ?? '', CLIENT_SECRET) + ) { + return json(response, 200, { error: 'incorrect_client_credentials' }); + } + const code = form.get('code') ?? ''; + const issued = codes.get(code); + codes.delete(code); + if (issued === undefined) return json(response, 200, { error: 'bad_verification_code' }); + if (form.get('redirect_uri') !== issued.redirectUri) { + return json(response, 200, { error: 'redirect_uri_mismatch' }); + } + if (issued.challenge !== null) { + const verifier = form.get('code_verifier') ?? ''; + if (issued.challengeMethod !== 'S256' || !same(s256(verifier), issued.challenge)) { + return json(response, 200, { error: 'bad_verification_code' }); + } + } + const accessToken = `gho_${randomBytes(27).toString('base64url')}`; + tokens.push(accessToken); + people.set(accessToken, issued.person); + return json(response, 200, { + access_token: accessToken, + token_type: 'bearer', + scope: 'user:email', + }); +} + +function bearer(request) { + const header = request.headers.authorization ?? ''; + return header.startsWith('Bearer ') ? people.get(header.slice('Bearer '.length)) : undefined; +} + +function api(request, url, response) { + if (request.headers['user-agent'] === undefined) { + return json(response, 403, { message: 'Request forbidden by administrative rules.' }); + } + const signedIn = bearer(request); + if (signedIn === undefined) return json(response, 401, { message: 'Bad credentials' }); + if (url.pathname === '/api/v3/user') { + return json(response, 200, { + login: signedIn.login, + id: signedIn.id, + node_id: `U_${String(signedIn.id)}`, + type: 'User', + name: signedIn.name, + email: null, + }); + } + return json(response, 200, signedIn.emails); +} + +async function control(request, url, response) { + if (request.method === 'PUT' && url.pathname === '/_control/person') { + person = JSON.parse(await bodyOf(request)); + return json(response, 200, person); + } + if (request.method === 'GET' && url.pathname === '/_control/tokens') { + return json(response, 200, { tokens }); + } + if (request.method === 'GET' && url.pathname === '/_control/calls') { + return json(response, 200, { calls }); + } + return json(response, 404, { message: 'Not Found' }); +} + +const tls = { + key: readFileSync(process.env.TLS_KEY ?? '/certs/mock.key'), + cert: readFileSync(process.env.TLS_CERT ?? '/certs/mock.crt'), +}; + +createServer(tls, (request, response) => { + const url = new URL(request.url ?? '/', 'https://mock-github'); + if (url.pathname.startsWith('/_control/')) { + control(request, url, response).catch(() => + json(response, 400, { message: 'bad control call' }) + ); + return; + } + calls.push({ method: request.method, host: request.headers.host ?? '', path: url.pathname }); + if (request.method === 'GET' && url.pathname === '/login/oauth/authorize') { + return authorize(url, response); + } + if (request.method === 'POST' && url.pathname === '/login/oauth/access_token') { + bodyOf(request) + .then((text) => token(request, new URLSearchParams(text), response)) + .catch(() => json(response, 400, { message: 'bad request' })); + return; + } + if ( + request.method === 'GET' && + (url.pathname === '/api/v3/user' || url.pathname === '/api/v3/user/emails') + ) { + return api(request, url, response); + } + return json(response, 404, { message: 'Not Found' }); +}).listen(PORT, '0.0.0.0', () => { + console.log(`mock GitHub listening on ${String(PORT)}`); +}); diff --git a/e2e/run.sh b/e2e/run.sh new file mode 100644 index 0000000..75bd463 --- /dev/null +++ b/e2e/run.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# The end-to-end suite: QAuth's released server image with this plugin +# installed the way an operator installs it, against a mock GitHub Enterprise +# Server, driven over HTTP by e2e/github.e2e.test.ts. +# +# Needs docker (with compose), openssl and node. Run it from anywhere: +# +# bash e2e/run.sh +# +# It packs the plugin as npm would publish it, makes throwaway secrets and a +# throwaway CA in e2e/.work/, builds the image (e2e/Dockerfile), starts the +# deployment (e2e/compose.yml), runs the suite and takes everything down. +# On a failure it prints the server's and the mock's logs. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(dirname "$here")" +work="$here/.work" +compose=(docker compose --file "$here/compose.yml") + +rm -rf "$work" +mkdir -p "$work/certs" "$work/ca" "$work/keys" "$work/pack" + +# A CA for this run only, and the mock's certificate for every name it is +# reached by: the two network names, and localhost for the suite on the host. +openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj '/CN=plugin-github e2e CA' \ + -addext 'basicConstraints=critical,CA:TRUE' -addext 'keyUsage=critical,keyCertSign' \ + -keyout "$work/ca/ca.key" -out "$work/certs/ca.crt" 2> /dev/null +openssl req -newkey rsa:2048 -nodes -subj '/CN=mock-github' \ + -keyout "$work/certs/mock.key" -out "$work/ca/mock.csr" 2> /dev/null +printf '%s\n' 'subjectAltName=DNS:mock-github,DNS:mock-github-unlisted,DNS:localhost' \ + 'extendedKeyUsage=serverAuth' > "$work/ca/mock.ext" +openssl x509 -req -in "$work/ca/mock.csr" -CA "$work/certs/ca.crt" -CAkey "$work/ca/ca.key" \ + -CAcreateserial -days 1 -extfile "$work/ca/mock.ext" -out "$work/certs/mock.crt" 2> /dev/null +# The mock reads its key through a read-only bind mount. +chmod 644 "$work/certs/mock.key" + +# The server's signing key and secrets, fresh for this run. +openssl genpkey -algorithm ed25519 -out "$work/keys/jwt-private.pem" 2> /dev/null +openssl pkey -in "$work/keys/jwt-private.pem" -pubout -out "$work/keys/jwt-public.pem" +JWT_PRIVATE_KEY="$(cat "$work/keys/jwt-private.pem")" +JWT_PUBLIC_KEY="$(cat "$work/keys/jwt-public.pem")" +KEK="$(openssl rand -base64 32)" +SESSION_COOKIE_SECRET="$(openssl rand -base64 48)" +MOCK_GITHUB_CLIENT_ID="Iv23e2eClient$(openssl rand -hex 4)" +MOCK_GITHUB_CLIENT_SECRET="$(openssl rand -hex 20)" +export JWT_PRIVATE_KEY JWT_PUBLIC_KEY KEK SESSION_COOKIE_SECRET +export MOCK_GITHUB_CLIENT_ID MOCK_GITHUB_CLIENT_SECRET + +# The allowlist is read through a bind mount; others must not be able to write it. +chmod 644 "$here/egress-allowlist.json" + +# The plugin as npm would receive it. +(cd "$root" && npm pack --silent --pack-destination "$work/pack" > /dev/null) +cp "$work"/pack/*.tgz "$work/plugin.tgz" + +finish() { + local status=$? + if [ "$status" -ne 0 ]; then + echo "--- the end-to-end suite failed; the deployment's logs follow ---" >&2 + "${compose[@]}" --profile plugin-api-off logs --no-color auth-server auth-server-plugin-api-off \ + mock-github migrate >&2 || true + fi + "${compose[@]}" --profile plugin-api-off down --volumes --remove-orphans > /dev/null 2>&1 || true + exit "$status" +} +trap finish EXIT + +wait_ready() { + local url="$1" + for _ in $(seq 1 120); do + if curl --silent --fail "$url" 2> /dev/null | grep -q '"status":"ok"'; then + return 0 + fi + sleep 1 + done + echo "$url did not become ready" >&2 + return 1 +} + +"${compose[@]}" build auth-server +"${compose[@]}" up --detach auth-server +wait_ready http://localhost:3000/ready +# The second server boots on the database the first one seeded. +"${compose[@]}" --profile plugin-api-off up --detach --no-build auth-server-plugin-api-off +wait_ready http://localhost:3010/ready + +cd "$root" +E2E_COMPOSE_FILE="$here/compose.yml" \ +E2E_WORK="$work" \ +NODE_EXTRA_CA_CERTS="$work/certs/ca.crt" \ + npx vitest run --config e2e/vitest.config.ts diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts new file mode 100644 index 0000000..881e7da --- /dev/null +++ b/e2e/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +// The end-to-end suite. It needs the deployment e2e/run.sh starts; run it +// through that script, not on its own. +export default defineConfig({ + test: { + include: ['e2e/**/*.e2e.test.ts'], + environment: 'node', + // One deployment, one realm: the tests run in order, in one file. + fileParallelism: false, + sequence: { concurrent: false }, + testTimeout: 60_000, + hookTimeout: 120_000, + }, +}); From e3ce5f34d10998c407288c6655ff7ddbfb7d4902 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:36:53 +0100 Subject: [PATCH 05/10] ci: check every pull request, and stage releases on npm from a tag ci.yml runs format, lint, typecheck, the unit tests with the contract test kit, the build and the tarball's file list on Node 22 and 26, then the end-to-end suite. release.yml, on a v* tag of a commit on main whose name is the package version, runs that CI, packs the tarball once and stages it on npm through trusted publishing for a maintainer's 2FA approval (next for a release candidate, latest for a release), then drafts the GitHub release. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- .github/workflows/ci.yml | 61 +++++++++++++ .github/workflows/release.yml | 159 ++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7a0beab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +# Every pull request, and the release before it stages anything (release.yml +# calls this workflow): format, lint, type-check, the unit tests and the +# plugin SDK's contract test kit (checkAuthMethod), the build and the +# tarball's file list; then the end-to-end suite, which runs the released +# QAuth server image with this plugin installed on top, against a mock +# GitHub (e2e/README.md). +name: ci + +on: + pull_request: + push: + branches: [main] + workflow_call: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + check: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + # The oldest Node the package supports, and the one QAuth's image runs. + node: [22, 26] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci --ignore-scripts + - run: npm run format:check + - run: npm run lint + - run: npm run typecheck + - name: Unit tests and the SDK's contract test kit (checkAuthMethod) + run: npm test + - run: npm run build + - name: The files npm would publish + run: npm pack --dry-run --ignore-scripts + + e2e: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 26 + cache: npm + - run: npm ci --ignore-scripts + - name: The released server image plus the plugin, against a mock GitHub + run: bash e2e/run.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bc0aa97 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,159 @@ +# The npm release of @qauth-labs/plugin-github, from a tag (RELEASING.md). +# +# A tag vX.Y.Z or vX.Y.Z-rc.N starts it. The tag must name the version in +# package.json. The whole CI runs first (ci.yml: the checks, the contract test +# kit, the end-to-end suite). Then the package is packed once, and that +# tarball, checked by its digest, is staged on npm through trusted publishing +# (OIDC, no token) in the `npm` environment; npm attaches provenance on its +# own. A release candidate goes to the `next` dist-tag, a release to +# `latest`. The staged version goes live only when a maintainer approves it +# on npmjs.com with 2FA. The run leaves a draft GitHub release with the +# tarball and the version's CHANGELOG section. +# +# npm's trusted publisher for @qauth-labs/plugin-github names this file: +# release.yml, environment npm. +name: release + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + ci: + uses: ./.github/workflows/ci.yml + + pack: + needs: ci + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.plan.outputs.version }} + dist-tag: ${{ steps.plan.outputs.dist-tag }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # The whole history: the tagged commit must be on main. + fetch-depth: 0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 26 + cache: npm + - name: The tag points at a commit on main + run: | + set -euo pipefail + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "The tag points at $GITHUB_SHA, which is not on main: only a reviewed, merged commit is released." >&2 + exit 1 + fi + - id: plan + name: The tag names the package's version; the dist-tag it goes to + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + version="$(node -p 'require("./package.json").version')" + if [ "$TAG" != "v$version" ]; then + echo "The tag $TAG does not name the package's version $version." >&2 + exit 1 + fi + case "$version" in + *-rc.*) dist_tag=next ;; + *-*) echo "Only a release candidate (-rc.N) or a release is published: $version." >&2; exit 1 ;; + *) dist_tag=latest ;; + esac + if ! grep -q "^## \[$version\]" CHANGELOG.md; then + echo "CHANGELOG.md has no section for $version." >&2 + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "dist-tag=$dist_tag" >> "$GITHUB_OUTPUT" + - run: npm ci --ignore-scripts + - name: Pack it as npm will receive it + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/pack" + npm pack --pack-destination "$RUNNER_TEMP/pack" + cd "$RUNNER_TEMP/pack" + tarball="$(ls ./*.tgz)" + sha256sum "$tarball" > "$tarball.sha256" + tar -tzf "$tarball" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: package + path: ${{ runner.temp }}/pack/ + if-no-files-found: error + retention-days: 7 + + publish: + needs: pack + runs-on: ubuntu-24.04 + environment: npm + permissions: + contents: read + id-token: write + env: + DIST_TAG: ${{ needs.pack.outputs.dist-tag }} + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: package + path: pack + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 26 + registry-url: https://registry.npmjs.org + - name: npm with staged publishing through OIDC + run: npm install --global npm@11.21.0 + - name: The tarball is the one the pack job made + working-directory: pack + run: sha256sum --check ./*.sha256 + - name: Stage it on npm; a maintainer approves it there, with 2FA + working-directory: pack + run: npm stage publish ./*.tgz --tag "$DIST_TAG" --access public + + draft-release: + needs: [pack, publish] + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + VERSION: ${{ needs.pack.outputs.version }} + DIST_TAG: ${{ needs.pack.outputs.dist-tag }} + TAG: ${{ github.ref_name }} + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: package + path: pack + - name: Draft the release with the tarball and the version's changelog section + run: | + set -euo pipefail + awk -v heading="## [$VERSION]" ' + index($0, heading) == 1 { found = 1; next } + found && /^## \[/ { exit } + found { print } + ' CHANGELOG.md > "$RUNNER_TEMP/notes.md" + { + echo + echo "---" + echo + echo "Staged on npm as \`$VERSION\` (dist-tag \`$DIST_TAG\`), with provenance. It goes" + echo "live when a maintainer approves the stage on npmjs.com with 2FA; publish this" + echo "draft after that." + } >> "$RUNNER_TEMP/notes.md" + prerelease=() + case "$VERSION" in *-rc.*) prerelease=(--prerelease) ;; esac + gh release create "$TAG" pack/*.tgz pack/*.sha256 \ + --draft --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" "${prerelease[@]}" From 9a1e54201bb3afb6b1e7ca4789bd13860638adc4 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:36:53 +0100 Subject: [PATCH 06/10] docs: the README (experimental), the release steps and the changelog The README says what the plugin does and stores, the PKCE finding, and how an operator installs it: the image recipe, AUTH_METHOD_PLUGINS, PLUGIN_API_ENABLED, the admin API configuration and the Enterprise Server allowlist. RELEASING.md gives the tag-driven release and the one-time npm trusted-publisher setup. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- CHANGELOG.md | 41 +++++++++ README.md | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++- RELEASING.md | 71 +++++++++++++++ 3 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 RELEASING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e325067 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to `@qauth-labs/plugin-github` are recorded here. The +format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The +package has its own version line, separate from QAuth's: 0.x while QAuth's +plugin API is experimental, with a `-rc.N` release candidate before each +release (RELEASING.md). + +## [Unreleased] + +## [0.1.0-rc.0] + +The first release candidate. Experimental: QAuth loads it only with +`PLUGIN_API_ENABLED=true`, and its plugin API may change before QAuth 1.0. + +### Added + +- Sign-in with GitHub as a QAuth AuthMethod plugin (contract + `qauth-auth-method/1-rc.0`), for QAuth 0.2.0-rc.1 and later on the 0.2 line. + It depends only on `@qauth-labs/plugin-sdk`. +- The OAuth 2.0 authorization code flow with `state` and PKCE (S256), on a + callback URL of this provider alone (`/upstream/github/callback`). The code + is redeemed at the token endpoint asking for JSON. +- The person is read from `GET /user` and `GET /user/emails`, with the + `user:email` scope and nothing more. The account is keyed by the numeric + user id under the GitHub instance that issued it, never by the login name. +- The email address becomes a verified attribute only when GitHub marks it + both primary and verified. The login name and the display name are stored + as unverified attributes. +- The access token is used for those two reads of one sign-in and dropped: + it is never stored, logged or handed to the server. +- A `baseUrl` for GitHub Enterprise Server. A private address is reached only + when the operator's egress allowlist names it for the realm. +- CI with the plugin SDK's contract test kit, and an end-to-end suite that + runs the released QAuth server image with the plugin installed, against a + mock GitHub. +- A release workflow that stages the package on npm through trusted + publishing, for a maintainer's approval with 2FA. + +[Unreleased]: https://github.com/qauth-labs/plugin-github/compare/v0.1.0-rc.0...HEAD +[0.1.0-rc.0]: https://github.com/qauth-labs/plugin-github/releases/tag/v0.1.0-rc.0 diff --git a/README.md b/README.md index 68a9053..f6a87b6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,242 @@ -# qauth-plugin-github -GitHub sign-in for QAuth, as an AuthMethod plugin (experimental) +# @qauth-labs/plugin-github + +> **Experimental.** This plugin runs on QAuth's experimental plugin API. QAuth +> loads it only while `PLUGIN_API_ENABLED=true`, and the API, the contract and +> this package may change before QAuth 1.0. Do not rely on it for production +> sign-in yet. + +Sign-in with GitHub for [QAuth](https://github.com/qauth-labs/qauth), as an +AuthMethod plugin. A realm that enables it offers "GitHub" as an upstream +sign-in: the person signs in on GitHub (or a GitHub Enterprise Server), and +QAuth signs them into the account that belongs to their GitHub user id. +Applications keep receiving QAuth's own OAuth 2.1 and OpenID Connect tokens. + +The plugin is made and released by the QAuth project, in its own repository, +the way a third party writes a plugin: it depends only on the published +[`@qauth-labs/plugin-sdk`](https://www.npmjs.com/package/@qauth-labs/plugin-sdk) +and has its own version line. + +## Compatibility + +| Plugin | QAuth server | AuthMethod contract | Plugin SDK | +| ------------ | ------------------------------- | -------------------------- | ------------ | +| `0.1.0-rc.0` | `0.2.0-rc.1` and later on `0.2` | `qauth-auth-method/1-rc.0` | `0.2.0-rc.1` | + +## How it signs a person in + +GitHub has no OpenID Connect sign-in: it issues no ID token and takes no +`nonce`. The plugin uses OAuth 2.0 and GitHub's REST API instead. + +1. **Authorization request.** The plugin sends the browser to + `/login/oauth/authorize` with a fresh `state` and a PKCE challenge + (S256), asking for the `user:email` scope and nothing else. The + `redirect_uri` is this provider's own callback, + `https:///upstream/github/callback`. +2. **Callback.** QAuth checks the returned `state` against the sign-in's flow + cookie before the plugin runs; the plugin checks it once more. With no ID + token, the `state`, the flow cookie and a callback URL that only this + provider uses are what tie GitHub's answer to the sign-in that asked for + it. +3. **Token exchange.** The code is redeemed at `/login/oauth/access_token` + with the PKCE verifier, the client id and secret, asking for JSON + (`Accept: application/json`). +4. **The person.** With the access token, the plugin reads `GET /user` (the + numeric id, the login, the display name) and `GET /user/emails` (the + addresses, which is what `user:email` allows). Then it drops the token. + +**The account is the numeric GitHub user id**, under the GitHub instance that +issued it (`https://github.com`, or the Enterprise Server's base URL). It is +never the login name: a login can be renamed and then taken by someone else. +A renamed login, or a changed email address, signs in the same account. + +**Attributes.** When the account is created, QAuth stores: + +| Attribute | From | Verified | +| -------------------- | ----------------------------------------- | ------------------------------------------- | +| `email` | the address GitHub marks **primary** | only when GitHub also marks it **verified** | +| `preferred_username` | the login name | never | +| `name` | the display name, when the person set one | never | + +Only the primary address is taken. An address that is verified but not +primary is not used, and a list with no primary address, or two, gives no +email at all. + +**The access token** is used for the two reads of one sign-in, and nowhere +else. It is never stored, logged or handed to QAuth. Each call that carries a +credential (the code and the client secret, or the access token) follows no +redirect. + +**Outbound calls.** Every call goes through the egress client QAuth hands the +plugin, never a network API of its own; the plugin holds no key. + +### PKCE + +The plugin always sends PKCE with S256 and the verifier at the token endpoint. + +- **github.com** accepts PKCE (S256) for OAuth apps and GitHub Apps since + GitHub's changelog of 2025-07-14, in the + [web application flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow). + It checks the verifier when the authorization request carried a challenge. +- **GitHub Enterprise Server** releases from before that change do not + support PKCE; they ignore the parameters they do not know (RFC 6749 §3.1). + There the sign-in rests on `state`, the flow cookie and the client secret, + as it did before PKCE. Check your release's documentation. + +The end-to-end suite runs against a mock that enforces PKCE the way +github.com does. It has not been run against github.com itself in CI, which +would need a real account. + +## Install it (operators) + +You need QAuth `0.2.0-rc.1` or later, and an image you build on top of +QAuth's released one. + +### 1. Register the app on GitHub + +Create an OAuth app (or a GitHub App) on github.com or on your Enterprise +Server: + +- **Callback URL**: `https:///upstream/github/callback`, one per + realm host. +- **OAuth app**: nothing else to set. The plugin asks for `user:email`. +- **GitHub App**: give it the account permission **Email addresses: read-only** + and no other permission. + +Keep the client id and generate a client secret. + +### 2. Add the package to the server image + +The image is a self-contained tree under `/app`. Install the plugin into a +separate stage, nested, so it brings its own copy of the plugin SDK, and copy +it in. `--omit=peer` makes it use the server's own `zod`; `--ignore-scripts` +runs no install script. Pin the server image by digest and the plugin by +version: + +```dockerfile +ARG QAUTH_IMAGE=ghcr.io/qauth-labs/qauth/auth-server:0.2.0-rc.1@sha256: + +# The build stage writes the files the server will run: pin it by digest too. +FROM node:26-alpine@sha256: AS plugin +WORKDIR /plugin +RUN npm init --yes > /dev/null \ + && npm install --omit=dev --omit=peer --install-strategy=nested --ignore-scripts \ + @qauth-labs/plugin-github@0.1.0-rc.0 + +FROM ${QAUTH_IMAGE} +COPY --from=plugin --chown=nodejs:nodejs \ + /plugin/node_modules/@qauth-labs/plugin-github \ + /app/node_modules/@qauth-labs/plugin-github +``` + +Rebuild it at every QAuth upgrade. To know the package was built by this +repository's release workflow, read the version's provenance on npmjs.com, +or the attestation at +`npm view @qauth-labs/plugin-github@ dist.attestations.url`, and +check that it names `qauth-labs/plugin-github` and +`.github/workflows/release.yml`. `npm audit signatures` alone checks that the +signatures and attestations are valid, not who built the package. QAuth's +own docs describe the recipe +under "Operate → Images → Add a plugin to the image". + +### 3. Turn it on + +```sh +AUTH_METHOD_PLUGINS=@qauth-labs/plugin-github +PLUGIN_API_ENABLED=true +``` + +The boot logs `AuthMethod plugin @qauth-labs/plugin-github loaded: github (github)`. +With `PLUGIN_API_ENABLED` off, the server does not load it and logs why: +`The AuthMethod plugin @qauth-labs/plugin-github is not loaded: it is not a +first-party plugin, and the experimental plugin API is off. …` + +### 4. Configure it for a realm + +Through QAuth's admin API, per realm (`{realm}` is the realm's id, `github` +the plugin's name): + +1. Seal the client secret: + `POST /admin/v1-rc.0/realms/{realm}/plugins/github/secrets/seal` with + `{"path": "clientSecret", "value": ""}`. The answer's + `sealed` value is what the server stores; the secret itself never is. +2. Set the configuration: + `POST /admin/v1-rc.0/realms/{realm}/plugins/github/config` with + `{"version": , "config": {"clientId": ""}, "secrets": {"clientSecret": {"sealed": ""}}}`. + Add `"baseUrl"` to `config` for an Enterprise Server. +3. Enable it: `POST /admin/v1-rc.0/realms/{realm}/plugins/github/enable` with + `{"version": }`. + +Setting the configuration and enabling the plugin are security operations: +each needs an approval (`QAuth-Approval` header), as QAuth's admin API +documentation describes. + +| Setting | Required | What it is | +| -------------- | -------- | ---------------------------------------------------------------------------------------- | +| `clientId` | yes | The app's client id. | +| `clientSecret` | yes | The app's client secret. Write-only: sealed by the server, never shown again. | +| `baseUrl` | no | An Enterprise Server's address, `https://host[:port]`, no path. Absent means github.com. | + +Once enabled, the realm's sign-in offers GitHub, and an application can send +the browser straight to it with `qauth_upstream=github` on +`/oauth/authorize`. A first sign-in creates an account when the realm allows +sign-up. + +### GitHub Enterprise Server + +Set `baseUrl` to the server's address, for example +`https://github.corp.example`. The plugin then uses +`/login/oauth/authorize`, `/login/oauth/access_token` and +the REST API at `/api/v3`. Accounts are keyed under that address, +so the same numeric id on github.com and on your server are two different +people. + +QAuth's egress client refuses private addresses (RFC 1918, unique-local and +the like). If your server is on a private address, the operator names it, for +the realms that may reach it, in the allowlist file `EGRESS_ALLOWLIST_PATH` +points at: + +```json +{ + "entries": [{ "host": "github.corp.example", "realms": ["master"] }] +} +``` + +Without that entry, a sign-in through that server fails with "We could not +sign you in", and the server logs `egress call blocked`. + +## Develop + +```sh +npm ci +npm run lint && npm run typecheck && npm test +``` + +`npm test` runs the unit tests against a fake GitHub, and the plugin SDK's +contract test kit (`checkAuthMethod`), for github.com and for an Enterprise +Server. + +The end-to-end suite needs docker, openssl and Node: + +```sh +bash e2e/run.sh +``` + +It packs the plugin, builds QAuth's released server image with the plugin +added as above, starts it with PostgreSQL, Redis and a mock GitHub +Enterprise Server, configures the plugin through the admin API and signs in +through the server's pages. It checks that: + +- the first sign-in creates an account keyed by the numeric id; +- a renamed login, and a changed email address, sign in the same account; +- only a primary and verified email becomes a verified attribute; +- the access token is not in the database, the logs or the events; +- a callback with a wrong `state` is refused; +- a private base URL is refused unless the allowlist names it; +- with `PLUGIN_API_ENABLED` off, the boot skips the plugin and says why. + +CI runs both on every pull request. Releases are described in +[RELEASING.md](RELEASING.md). + +## License + +[Apache-2.0](LICENSE) diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..520c9a9 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,71 @@ +# Releasing + +`@qauth-labs/plugin-github` is released to npm from a tag of this repository, +by `.github/workflows/release.yml`, through npm's trusted publishing: the +workflow proves who it is with GitHub's OIDC token, holds no npm token, and +npm attaches provenance. The workflow only **stages** the version; it goes +live when a maintainer approves the stage on npmjs.com with 2FA. This is the +way QAuth's own SDK packages are released (`release-npm.yml` in +qauth-labs/qauth). + +## Versions + +The package has its own version line, not QAuth's. + +- `0.x` while QAuth's plugin API is experimental (`PLUGIN_API_ENABLED`). +- Each release is preceded by release candidates, `X.Y.Z-rc.N`, which go to + the `next` dist-tag. A release goes to `latest`. No other pre-release form + is published. +- The README's compatibility table says which QAuth versions and which + AuthMethod contract each version is for. Update it in the same pull request + as the version. + +## Cut a release + +1. In a pull request, set the version and its changelog section: + + ```sh + npm version --no-git-tag-version 0.1.0-rc.1 + ``` + + Move the `Unreleased` entries of `CHANGELOG.md` under `## [0.1.0-rc.1]`, + update the links at the bottom and the README's compatibility table. Merge + it when CI is green. + +2. Tag the merge commit on `main` and push the tag: + + ```sh + git tag -s v0.1.0-rc.1 -m v0.1.0-rc.1 + git push origin v0.1.0-rc.1 + ``` + +3. `release.yml` runs the whole CI again (checks, contract test kit, + end-to-end suite), checks that the tag points at a commit on `main`, that + it names the version in `package.json` and that `CHANGELOG.md` has its + section, packs the tarball once and stages + it on npm (`npm stage publish`), from the `npm` environment. + +4. Approve the stage on npmjs.com with 2FA. Check the version, the dist-tag + and the provenance before approving. + +5. Publish the draft GitHub release the run left, with the tarball and the + changelog section. + +## One-time setup + +npm configures a trusted publisher only for a package that exists. Before the +first release, a maintainer: + +1. Publishes a placeholder by hand, from an empty directory, once: + `@qauth-labs/plugin-github@0.0.0` with only a `package.json` and a README + that points here. +2. On npmjs.com, under the package's settings, adds the trusted publisher: + the organisation `qauth-labs`, the repository `plugin-github`, the + workflow `release.yml`, the environment `npm`. Then sets publishing access + to require 2FA and disallow tokens. +3. In this repository's settings, creates the `npm` environment and limits + it to tags matching `v*`, and adds a tag ruleset that lets only + maintainers create or move `v*` tags. + +Nothing else holds a credential: the workflow's `id-token: write` permission +is granted only to the job that stages the package. From e38fc311c4071c791a157394f613a93f23b16cf7 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:47:12 +0100 Subject: [PATCH 07/10] build: use QAuth's toolchain and dependency versions The plugin is part of QAuth's stack, so it is built the way QAuth is: pnpm (pinned through packageManager) with QAuth's 7-day release-age cooldown, Oxlint with QAuth's rule set and Oxfmt with its options instead of ESLint and Prettier, Node 26.10 or later (the Node QAuth requires and its image runs) and the same TypeScript, Vitest, @types/node and zod ranges as QAuth's catalog. CI runs on Node 26 only. The release still stages the tarball with npm, as QAuth's own release-npm.yml does: pnpm builds and packs it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- .editorconfig | 15 + .github/workflows/ci.yml | 45 +- .github/workflows/release.yml | 9 +- .oxfmtrc.json | 15 + .oxlintrc.json | 128 ++ .prettierignore | 5 - .prettierrc.json | 5 - README.md | 15 +- RELEASING.md | 13 +- e2e/harness.ts | 4 +- eslint.config.js | 70 - package-lock.json | 2502 --------------------------------- package.json | 25 +- pnpm-lock.yaml | 1891 +++++++++++++++++++++++++ pnpm-workspace.yaml | 17 + test/method.test.ts | 2 +- 16 files changed, 2125 insertions(+), 2636 deletions(-) create mode 100644 .editorconfig create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json delete mode 100644 .prettierignore delete mode 100644 .prettierrc.json delete mode 100644 eslint.config.js delete mode 100644 package-lock.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..63dfdc7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 100 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a0beab..8b486a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ # Every pull request, and the release before it stages anything (release.yml -# calls this workflow): format, lint, type-check, the unit tests and the -# plugin SDK's contract test kit (checkAuthMethod), the build and the -# tarball's file list; then the end-to-end suite, which runs the released -# QAuth server image with this plugin installed on top, against a mock -# GitHub (e2e/README.md). +# calls this workflow): format (Oxfmt), lint (Oxlint), type-check, the unit +# tests and the plugin SDK's contract test kit (checkAuthMethod), the build +# and the tarball's file list; then the end-to-end suite (e2e/run.sh), which +# runs the released QAuth server image with this plugin installed on top, +# against a mock GitHub. pnpm's version is package.json's packageManager. name: ci on: @@ -22,28 +22,28 @@ concurrency: jobs: check: runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - # The oldest Node the package supports, and the one QAuth's image runs. - node: [22, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: ${{ matrix.node }} - cache: npm - - run: npm ci --ignore-scripts - - run: npm run format:check - - run: npm run lint - - run: npm run typecheck + # The Node QAuth's server image runs, and the plugin with it. + node-version: 26 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run format:check + - run: pnpm run lint + - run: pnpm run typecheck - name: Unit tests and the SDK's contract test kit (checkAuthMethod) - run: npm test - - run: npm run build - - name: The files npm would publish - run: npm pack --dry-run --ignore-scripts + run: pnpm test + - run: pnpm run build + - name: The files npm would receive + run: | + set -euo pipefail + pnpm pack --pack-destination "$RUNNER_TEMP/pack" + tar -tzf "$RUNNER_TEMP"/pack/*.tgz e2e: runs-on: ubuntu-24.04 @@ -52,10 +52,11 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 26 - cache: npm - - run: npm ci --ignore-scripts + cache: pnpm + - run: pnpm install --frozen-lockfile - name: The released server image plus the plugin, against a mock GitHub run: bash e2e/run.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc0aa97..bf0f5e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,10 +41,11 @@ jobs: persist-credentials: false # The whole history: the tagged commit must be on main. fetch-depth: 0 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 26 - cache: npm + cache: pnpm - name: The tag points at a commit on main run: | set -euo pipefail @@ -74,12 +75,13 @@ jobs: fi echo "version=$version" >> "$GITHUB_OUTPUT" echo "dist-tag=$dist_tag" >> "$GITHUB_OUTPUT" - - run: npm ci --ignore-scripts + - run: pnpm install --frozen-lockfile + - run: pnpm run build - name: Pack it as npm will receive it run: | set -euo pipefail mkdir -p "$RUNNER_TEMP/pack" - npm pack --pack-destination "$RUNNER_TEMP/pack" + pnpm pack --pack-destination "$RUNNER_TEMP/pack" cd "$RUNNER_TEMP/pack" tarball="$(ls ./*.tgz)" sha256sum "$tarball" > "$tarball.sha256" @@ -109,6 +111,7 @@ jobs: with: node-version: 26 registry-url: https://registry.npmjs.org + # pnpm builds and packs; npm stages the tarball, as QAuth's release-npm.yml does. - name: npm with staged publishing through OIDC run: npm install --global npm@11.21.0 - name: The tarball is the one the pack job made diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..cec16af --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,15 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + // QAuth's own formatting options (qauth-labs/qauth's .oxfmtrc.json). + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf", + "sortPackageJson": false, + // Gitignore-style; .gitignore applies too. + "ignorePatterns": ["node_modules", "dist", "coverage", "*.log", "pnpm-lock.yaml", "e2e/.work"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..788ed04 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,128 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + // QAuth's own rule set (qauth-labs/qauth's .oxlintrc.json): @eslint/js and + // typescript-eslint `recommended` as Nx configures them, and + // simple-import-sort, less the Nx module boundaries and QAuth's key custody + // guard, which concern QAuth's own workspace. Oxlint's default category is + // off, so nothing else runs. Formatting is Oxfmt's job (.oxfmtrc.json). + "plugins": ["eslint", "typescript"], + "jsPlugins": ["eslint-plugin-simple-import-sort"], + "categories": { + "correctness": "off" + }, + "ignorePatterns": ["**/coverage/**", "**/dist/**", "**/node_modules/**", "e2e/.work/**"], + "rules": { + "for-direction": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": ["error", { "checkLoops": "allExceptWhileTrue" }], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-else-if": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-fallthrough": "error", + "no-global-assign": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-nonoctal-decimal-escape": "error", + "no-prototype-builtins": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-shadow-restricted-names": ["error", { "reportGlobalThis": true }], + "no-sparse-arrays": "error", + "no-unassigned-vars": "error", + "no-unsafe-finally": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-useless-assignment": "error", + "no-useless-backreference": "error", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-var": "error", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "preserve-caught-error": "error", + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + + "no-array-constructor": "error", + "no-empty-function": "error", + "no-unused-expressions": "error", + "no-unused-vars": [ + "warn", + { "vars": "all", "args": "after-used", "caughtErrors": "all", "ignoreRestSiblings": false } + ], + "typescript/adjacent-overload-signatures": "error", + "typescript/ban-ts-comment": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-empty-interface": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "warn", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-inferrable-types": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-non-null-assertion": "warn", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error", + + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error" + }, + "overrides": [ + { + // Every outbound call goes through the egress client the server hands + // the plugin (ADR-019 Decision 8), never a network API of its own. + "files": ["src/**/*.ts"], + "rules": { + "no-restricted-globals": [ + "error", + { + "name": "fetch", + "message": "Use context.egress: every call goes through the egress client." + } + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "node:http", "message": "Use context.egress." }, + { "name": "node:https", "message": "Use context.egress." }, + { "name": "node:net", "message": "Use context.egress." }, + { "name": "node:tls", "message": "Use context.egress." }, + { "name": "node:dgram", "message": "Use context.egress." }, + { "name": "http", "message": "Use context.egress." }, + { "name": "https", "message": "Use context.egress." }, + { "name": "net", "message": "Use context.egress." }, + { "name": "tls", "message": "Use context.egress." }, + { "name": "dgram", "message": "Use context.egress." }, + { "name": "undici", "message": "Use context.egress." } + ] + } + ] + } + } + ] +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index f0de231..0000000 --- a/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -dist/ -node_modules/ -coverage/ -package-lock.json -CHANGELOG.md diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index ae7544d..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "singleQuote": true, - "printWidth": 100, - "trailingComma": "es5" -} diff --git a/README.md b/README.md index f6a87b6..5381c0d 100644 --- a/README.md +++ b/README.md @@ -206,19 +206,24 @@ sign you in", and the server logs `egress call blocked`. ## Develop +The repository uses QAuth's own toolchain: Node 26, pnpm (the version in +`package.json`'s `packageManager`), TypeScript, Oxlint, Oxfmt and Vitest. +pnpm resolves a dependency version only once it has been on the registry +for 7 days (`pnpm-workspace.yaml`), as QAuth does. + ```sh -npm ci -npm run lint && npm run typecheck && npm test +pnpm install +pnpm run lint && pnpm run format:check && pnpm run typecheck && pnpm test ``` -`npm test` runs the unit tests against a fake GitHub, and the plugin SDK's +`pnpm test` runs the unit tests against a fake GitHub, and the plugin SDK's contract test kit (`checkAuthMethod`), for github.com and for an Enterprise Server. -The end-to-end suite needs docker, openssl and Node: +The end-to-end suite needs docker, openssl, Node and pnpm: ```sh -bash e2e/run.sh +pnpm run e2e ``` It packs the plugin, builds QAuth's released server image with the plugin diff --git a/RELEASING.md b/RELEASING.md index 520c9a9..c7d9415 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -22,13 +22,8 @@ The package has its own version line, not QAuth's. ## Cut a release -1. In a pull request, set the version and its changelog section: - - ```sh - npm version --no-git-tag-version 0.1.0-rc.1 - ``` - - Move the `Unreleased` entries of `CHANGELOG.md` under `## [0.1.0-rc.1]`, +1. In a pull request, set `version` in `package.json` (for example + `0.1.0-rc.1`) and move the `Unreleased` entries of `CHANGELOG.md` under `## [0.1.0-rc.1]`, update the links at the bottom and the README's compatibility table. Merge it when CI is green. @@ -42,8 +37,8 @@ The package has its own version line, not QAuth's. 3. `release.yml` runs the whole CI again (checks, contract test kit, end-to-end suite), checks that the tag points at a commit on `main`, that it names the version in `package.json` and that `CHANGELOG.md` has its - section, packs the tarball once and stages - it on npm (`npm stage publish`), from the `npm` environment. + section, builds and packs the tarball once with pnpm and stages it on npm + (`npm stage publish`), from the `npm` environment. 4. Approve the stage on npmjs.com with 2FA. Check the version, the dist-tag and the provenance before approving. diff --git a/e2e/harness.ts b/e2e/harness.ts index ccc8e9d..30ca8cb 100644 --- a/e2e/harness.ts +++ b/e2e/harness.ts @@ -10,15 +10,17 @@ import { createHash, createPrivateKey, generateKeyPairSync, - type JsonWebKey, type KeyObject, randomBytes, randomUUID, sign, + type webcrypto, } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; +type JsonWebKey = webcrypto.JsonWebKey; + /** The server's public listener, as the suite reaches it. */ export const SERVER = 'http://localhost:3000'; /** The admin API's listener. */ diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 961d0af..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,70 +0,0 @@ -import js from '@eslint/js'; -import prettier from 'eslint-config-prettier'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { ignores: ['dist/', 'node_modules/', 'coverage/'] }, - js.configs.recommended, - ...tseslint.configs.strictTypeChecked, - { - languageOptions: { - parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }, - }, - }, - { - rules: { - // The AuthMethod contract's members are async by their type; one that - // has nothing to wait for is still async. - '@typescript-eslint/require-await': 'off', - }, - }, - { - files: ['src/**/*.ts'], - rules: { - // Every outbound call goes through the egress client the server hands - // the plugin (ADR-019 Decision 8), never a network API of its own. - 'no-restricted-globals': [ - 'error', - { - name: 'fetch', - message: 'Use context.egress: every call goes through the egress client.', - }, - ], - 'no-restricted-imports': [ - 'error', - { - paths: [ - 'node:http', - 'node:https', - 'node:net', - 'node:tls', - 'http', - 'https', - 'net', - 'tls', - 'undici', - ].map((name) => ({ - name, - message: 'Use context.egress: every call goes through the egress client.', - })), - }, - ], - }, - }, - { - files: ['**/*.js', '**/*.mjs'], - ...tseslint.configs.disableTypeChecked, - }, - { - // The plain-JavaScript files (this config, the mock GitHub) run on Node. - files: ['**/*.js', '**/*.mjs'], - languageOptions: { - globals: Object.fromEntries( - ['Buffer', 'URL', 'URLSearchParams', 'console', 'process', 'structuredClone'].map( - (name) => [name, 'readonly'] - ) - ), - }, - }, - prettier -); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b759829..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2502 +0,0 @@ -{ - "name": "@qauth-labs/plugin-github", - "version": "0.1.0-rc.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@qauth-labs/plugin-github", - "version": "0.1.0-rc.0", - "license": "Apache-2.0", - "dependencies": { - "@qauth-labs/plugin-sdk": "0.2.0-rc.1" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^22.19.0", - "eslint": "^10.14.0", - "eslint-config-prettier": "^10.1.8", - "prettier": "^3.9.10", - "typescript": "~6.0.3", - "typescript-eslint": "^8.72.1", - "vitest": "^5.1.0", - "zod": "^4.6.7" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "zod": "^4.6.7" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", - "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.5.0", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", - "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", - "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.156.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", - "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@qauth-labs/plugin-sdk": { - "version": "0.2.0-rc.1", - "resolved": "https://registry.npmjs.org/@qauth-labs/plugin-sdk/-/plugin-sdk-0.2.0-rc.1.tgz", - "integrity": "sha512-HzJdOkoteT1sd8y6iOQ0HVpdBNB1kNuSXQFlhu6CwS8ZEFgRN05Hrx+TLBzQagwig7fXGAQVxj37khOJbjxS/w==", - "license": "Apache-2.0", - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "zod": "^4.6.7" - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", - "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", - "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", - "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", - "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", - "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", - "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", - "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", - "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", - "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", - "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", - "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", - "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", - "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", - "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", - "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", - "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.20.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", - "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", - "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.72.1", - "@typescript-eslint/type-utils": "8.72.1", - "@typescript-eslint/utils": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.72.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", - "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", - "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.72.1", - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", - "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.72.1", - "@typescript-eslint/types": "^8.72.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", - "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", - "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", - "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1", - "@typescript-eslint/utils": "8.72.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", - "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", - "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.72.1", - "@typescript-eslint/tsconfig-utils": "8.72.1", - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", - "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.72.1", - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", - "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/mocker": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz", - "integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.1.0", - "estree-walker": "^3.0.3", - "magic-string": "^1.4.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9 || ^3.0.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/spy": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz", - "integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", - "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/cacheable": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", - "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.2.0", - "@cacheable/utils": "^2.5.0", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, - "node_modules/chai": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", - "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz", - "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.3", - "@humanfs/node": "^0.16.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "11.1.5 || >11.1.6 <12", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.6", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", - "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^6.1.23" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "6.1.23", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", - "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cacheable": "^2.5.0", - "flatted": "^3.4.2", - "hookified": "^1.15.0" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/magic-string": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", - "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", - "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", - "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", - "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "nanoid": "^3.3.20", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", - "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rolldown": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", - "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@oxc-project/types": "=0.156.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.16", - "@rolldown/binding-android-arm64": "1.2.16", - "@rolldown/binding-darwin-arm64": "1.2.16", - "@rolldown/binding-darwin-x64": "1.2.16", - "@rolldown/binding-freebsd-x64": "1.2.16", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", - "@rolldown/binding-linux-arm64-gnu": "1.2.16", - "@rolldown/binding-linux-arm64-musl": "1.2.16", - "@rolldown/binding-linux-ppc64-gnu": "1.2.16", - "@rolldown/binding-linux-s390x-gnu": "1.2.16", - "@rolldown/binding-linux-x64-gnu": "1.2.16", - "@rolldown/binding-linux-x64-musl": "1.2.16", - "@rolldown/binding-openharmony-arm64": "1.2.16", - "@rolldown/binding-win32-arm64-msvc": "1.2.16", - "@rolldown/binding-win32-x64-msvc": "1.2.16" - } - }, - "node_modules/semver": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", - "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.3.tgz", - "integrity": "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/tinyexec": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", - "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", - "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.72.1", - "@typescript-eslint/parser": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1", - "@typescript-eslint/utils": "8.72.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", - "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.32", - "rolldown": "~1.2.16", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1 || ^0.8.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz", - "integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/mocker": "5.1.0", - "chai": "^6.3.0", - "es-module-lexer": "^2.3.2", - "expect-type": "^1.4.0", - "magic-string": "^1.4.3", - "obug": "^3.0.0", - "picomatch": "^4.0.7", - "std-env": "^4.3.0", - "tinybench": "^6.2.2", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "why-is-node-running": "3.2.1" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^22.12.0 || ^24.0.0 || >=26.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.1.0", - "@vitest/browser-preview": "5.1.0", - "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.1.0", - "@vitest/coverage-v8": "5.1.0", - "@vitest/ui": "5.1.0", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", - "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", - "dev": true, - "license": "MIT", - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=20.11" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", - "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json index c489671..a6fc142 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "type": "module", "sideEffects": false, "engines": { - "node": ">=22" + "node": ">=26.10.0" }, + "packageManager": "pnpm@12.14.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -42,12 +43,12 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "typecheck": "tsc -p tsconfig.json --noEmit", - "lint": "eslint .", - "format": "prettier --write .", - "format:check": "prettier --check .", + "lint": "oxlint", + "format": "oxfmt", + "format:check": "oxfmt --check", "test": "vitest run", - "e2e": "vitest run --config e2e/vitest.config.ts", - "prepack": "npm run build" + "e2e": "bash e2e/run.sh", + "prepack": "pnpm run build" }, "dependencies": { "@qauth-labs/plugin-sdk": "0.2.0-rc.1" @@ -56,14 +57,12 @@ "zod": "^4.6.7" }, "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^22.19.0", - "eslint": "^10.14.0", - "eslint-config-prettier": "^10.1.8", - "prettier": "^3.9.10", + "@types/node": "^26.6.5", + "eslint-plugin-simple-import-sort": "^14.0.0", + "oxfmt": "0.73.0", + "oxlint": "1.88.0", "typescript": "~6.0.3", - "typescript-eslint": "^8.72.1", - "vitest": "^5.1.0", + "vitest": "^5.0.5", "zod": "^4.6.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..bc71138 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1891 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.14.0 + version: 12.14.0 + +packages: + + '@pnpm/exe.android-arm64@12.14.0': + resolution: {integrity: sha512-+iSh7x21C3A2hqc+Z0ZmbKTcn6YNlzrXM142PsnQH/dLj0DYxFzoBvc6XAnL/yfQ5nnH2ebBiRj/zVxr61fZbQ==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.14.0': + resolution: {integrity: sha512-qT+0LqEr2wBVaxcG2T8cX/R8SSRz2Rvpp8nRR4fh8tnGUZRch87gFmsHUAoSQh7w6j1h0qTjrA384j4xi8frvw==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.14.0': + resolution: {integrity: sha512-tnrdUB8ecJoehuKKqi1YHCBVl61xtJ9XVGcx5EviC6fWO4DeGI8GtXrjvJxDBM3nuVjJeWistgsUp/N142x4sw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.14.0': + resolution: {integrity: sha512-vlBfGkruqQRsEHTW43cPwpYd7QlbIJ//cvpAdb3bVXkI8SgTPoXhCnc05m+ZNiPrrEkBsX/t7KHw9aXpkzPm3Q==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.14.0': + resolution: {integrity: sha512-Qg5rxBrGYl6Tfso+EyygPf84fY1Fzy55E/0or+iuYKtnf0vc0MlHJfVIlT7pBukMRntQHRik7hUi0Z4s7AlJsw==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.14.0': + resolution: {integrity: sha512-DRIe4yQkXBGA/PhVJdzlbnllxBdAtnMMvTVLLcGpsK5FvJz9kxD2Hyxtn68ovmNkVX06f6p45cQwfm3wx2jn1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.14.0': + resolution: {integrity: sha512-7joGShZ44Nk3WpNU+60JoOPjCXlgxhJAq11S2UiVtwCUDgTer1yTPPfxGW8pxaD6046actYLIYDbZNlubHWs0A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.14.0': + resolution: {integrity: sha512-gSoryn7ySdXd//gjgf3MvvA8ZerJayaSbuMZMtAWq4vb2/wlVEgpl0Kvqt54y58TRj533Qjq2txKgnxd4JyoAQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.14.0': + resolution: {integrity: sha512-EMjew1H9WqQ7pL+tYqCPwiuB+0PeCiJRV7fyWeuFw0wwYUajFBjcaPO3IGAVu9sdmJDU+e+HYv/Zey6aKae0Vw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.14.0': + resolution: {integrity: sha512-shYOj/SFoXD35fHe0pWr2JofwSXulC0BnEIXLj2QokDi0EF//AfKsui35yKIkTEl+s2VJybc3PO4iJNhKIj5Qw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.14.0': + resolution: {integrity: sha512-btZiZ2Ot0llKK1ndSfA5i3IRX7/S0VrmzseNH/BJ4c+Cady+S81H6vvU1zByggHMyqQhC8OgxO5PiIY7D9bH0Q==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.14.0': + resolution: {integrity: sha512-F6K8e/jWxiuEYJaQtRRUG9ynBSTEHa+t7Lf86MKuOO5UA7WVVMbi+oB8nY6iKW++ncWS2yG6T9faXZGxqNsfoQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.14.0': + resolution: {integrity: sha512-DOjlkVB6TtTKeAAe0hNZXn5S7YqESmo1Z6SGbR4BZNQ4isRg31XVHjX2RFTbY8gnZq68IwzyLmPqlEjMmYxofg==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.14.0': + resolution: {integrity: sha512-/LSX6mew/sLf08XqcjemuklNKf1K/Ftpgj4//hK/HKzI2dKbmxDB27a/2OaQ5a0O3/3OFzlssXDziZsi+yVBwQ==} + cpu: [x64] + os: [win32] + + pnpm@12.14.0: + resolution: {integrity: sha512-wYum2HmT09Fky16JSPMrLAbuhUSH4GzY2GeBNC+z5btGNDZwNHEehoArqpw3vqkGzhkprQjXCITJfYAd8TOJpg==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.14.0': + optional: true + + '@pnpm/exe.android-x64@12.14.0': + optional: true + + '@pnpm/exe.darwin-arm64@12.14.0': + optional: true + + '@pnpm/exe.darwin-x64@12.14.0': + optional: true + + '@pnpm/exe.freebsd-x64@12.14.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.14.0': + optional: true + + '@pnpm/exe.linux-arm64@12.14.0': + optional: true + + '@pnpm/exe.linux-ppc64@12.14.0': + optional: true + + '@pnpm/exe.linux-riscv64@12.14.0': + optional: true + + '@pnpm/exe.linux-s390x@12.14.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.14.0': + optional: true + + '@pnpm/exe.linux-x64@12.14.0': + optional: true + + '@pnpm/exe.win32-arm64@12.14.0': + optional: true + + '@pnpm/exe.win32-x64@12.14.0': + optional: true + + pnpm@12.14.0: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.14.0 + '@pnpm/exe.android-x64': 12.14.0 + '@pnpm/exe.darwin-arm64': 12.14.0 + '@pnpm/exe.darwin-x64': 12.14.0 + '@pnpm/exe.freebsd-x64': 12.14.0 + '@pnpm/exe.linux-arm64': 12.14.0 + '@pnpm/exe.linux-arm64-musl': 12.14.0 + '@pnpm/exe.linux-ppc64': 12.14.0 + '@pnpm/exe.linux-riscv64': 12.14.0 + '@pnpm/exe.linux-s390x': 12.14.0 + '@pnpm/exe.linux-x64': 12.14.0 + '@pnpm/exe.linux-x64-musl': 12.14.0 + '@pnpm/exe.win32-arm64': 12.14.0 + '@pnpm/exe.win32-x64': 12.14.0 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@qauth-labs/plugin-sdk': + specifier: 0.2.0-rc.1 + version: 0.2.0-rc.1(zod@4.6.7) + devDependencies: + '@types/node': + specifier: ^26.6.5 + version: 26.6.6 + eslint-plugin-simple-import-sort: + specifier: ^14.0.0 + version: 14.0.0(eslint@10.13.0) + oxfmt: + specifier: 0.73.0 + version: 0.73.0 + oxlint: + specifier: 1.88.0 + version: 1.88.0 + typescript: + specifier: ~6.0.3 + version: 6.0.3 + vitest: + specifier: ^5.0.5 + version: 5.0.5(@types/node@26.6.6)(vite@8.3.5(@types/node@26.6.6)) + zod: + specifier: ^4.6.7 + version: 4.6.7 + +packages: + + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@oxc-project/types@0.155.0': + resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} + + '@oxfmt/binding-android-arm-eabi@0.73.0': + resolution: {integrity: sha512-dhunAdMbi/4o0Y66/+Wl6rpsNqpvE4i26Qw6bB77A97L2lJoktWg6m+X9QOon8iOEEqzD9rEZqFi5uZ4oIzhCA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.73.0': + resolution: {integrity: sha512-dBoypjC2/xeyKyIhN2waX0/XeoccH9k0lVVe+sGRc4X8HSJVRtqyZDoXdr7bGY9A75c51dX0z0dLDpGQKwCdCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.73.0': + resolution: {integrity: sha512-Tn7Y+qWIpCQK+qdYwBcn6YvqeBuPiuG3tcNlYg16a5QL2o3rSmrgBB1nCyYqo0uvc1LNrfQW9/CK2tzFuID/Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.73.0': + resolution: {integrity: sha512-yVb0E0tg/+9Xc9YJCBq66qF1DC/aN5mhb5nnHz0k4ikwkMANtSul2VPDSwjYdXR6/DFimc+pPUgLBqkIa5Ctuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.73.0': + resolution: {integrity: sha512-IKR4lFnBkYNL9Uqdxa5GMTQLvnLj+3sDwTvh2atHCzah0xeo+1xR5bigRU8uyXlOsA4VpXUn0UAimq4E4RBGmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.73.0': + resolution: {integrity: sha512-7qnZ4g0ZCO3aZevOOQpZIABYwwXJW1lLDo2elviGdPQ0i11255xKEMFsi97fC1/nauQFseCJfELUy/k2JoOP+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.73.0': + resolution: {integrity: sha512-36eBico2j/WPB84gb4FYd/jjqGCKOwU4M+TWLBUIqlnzTozQueLy0joA5BwsSp2OZFVkOlpCwvA3ndPamdiyLw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.73.0': + resolution: {integrity: sha512-VZ3cSgCAT18knKo52DcACmAuIoqG6qVEyeZRFbJd0xmYQ8GyYjuBc6IAh6MZHijT6q7p4/HPI+6ZWsCuhdV6iw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.73.0': + resolution: {integrity: sha512-8YVzH59XSHcqwXNgc5qK851zCqwpmBgOifARXEPPe5/xTsIbHceYoPZAyqfpYV0CfKmzSaEDBgYHFza4rhx+Zw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.73.0': + resolution: {integrity: sha512-MqRJnmCys0rwOgpWyEidSA7v/bvOqfNDJhPkQoR9mPVYrvSmMaW2Xhdpl28cbTA+brOM1is62GjIvJplVlUVpw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.73.0': + resolution: {integrity: sha512-4t2F3SWJSK/O/kRRFnL0elJzn75fJGGu71gs1HtiWcS80SmJOpkHiwwLnTp9suGdns8VHEPxYS0GTcyVwHKWBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.73.0': + resolution: {integrity: sha512-D8b4I8I1kLdJQFwdFXNV+cwh3xWSiNKofjdGUk0nDpc0AHOds8T+poGbP9RWQMvgsvMb9OMgp/aV7ahBR37TbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.73.0': + resolution: {integrity: sha512-QrC1S5VL1+N5rvorB15jH7CPvjVHA7GyUI5oVEd8Bd9m5605RDBQYTxDQ920DuylRoZOh2V7/xXpfCtFUrojUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.73.0': + resolution: {integrity: sha512-dFcUKQPltCEPa+YlkF3zqufn6nJPZdG73LpnjBvZFFsU3laCMssDXSYB99mVaj43FqdEgjpz5TO6gbYXcczd9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.73.0': + resolution: {integrity: sha512-LmQe+nV8e+eUwfPNCxDF+owbsg88toBiR/8rZRgVOeKQP3MaGefXZRvBoc1UG37iWEQ/9JlCXG9Mc4nf4N3agw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.73.0': + resolution: {integrity: sha512-8weOr1OKknE0G2DbR6ejJUT9AOsoBU6cXlsetmbihHv1K+bK7qP61T6lJxY0k/aAO7GyOJiRGNlL+04r4eH8Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.73.0': + resolution: {integrity: sha512-mZV+QxXYgb8E18gzAFItb937VIsE4dqWdStrD3tRRC52UubOYEuU+e7NaIqUsGdB5YsJqNeuck3Ue/af2r9wyA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.73.0': + resolution: {integrity: sha512-QJPUsbgUb97ZPPMkFUq6VQq0fLhC4Np19KL9aB/qKVwXnpKE6MIiILXqzhKnQ/UbnEDjKJ7Xv1d6zH4bYPCUdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.73.0': + resolution: {integrity: sha512-cGcze2fu+ykIaFtkEvZlkZ3HvnCLk+WISthtq99zWTWn0x13t5L9TxvGdLZMUMvtfYR+ICkiCwKnguVubdcw0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.88.0': + resolution: {integrity: sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.88.0': + resolution: {integrity: sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.88.0': + resolution: {integrity: sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.88.0': + resolution: {integrity: sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.88.0': + resolution: {integrity: sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.88.0': + resolution: {integrity: sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.88.0': + resolution: {integrity: sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.88.0': + resolution: {integrity: sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.88.0': + resolution: {integrity: sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.88.0': + resolution: {integrity: sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.88.0': + resolution: {integrity: sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.88.0': + resolution: {integrity: sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.88.0': + resolution: {integrity: sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.88.0': + resolution: {integrity: sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.88.0': + resolution: {integrity: sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.88.0': + resolution: {integrity: sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.88.0': + resolution: {integrity: sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.88.0': + resolution: {integrity: sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.88.0': + resolution: {integrity: sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@qauth-labs/plugin-sdk@0.2.0-rc.1': + resolution: {integrity: sha512-HzJdOkoteT1sd8y6iOQ0HVpdBNB1kNuSXQFlhu6CwS8ZEFgRN05Hrx+TLBzQagwig7fXGAQVxj37khOJbjxS/w==} + engines: {node: '>=22'} + peerDependencies: + zod: ^4.6.7 + + '@rolldown/binding-android-arm-eabi@1.2.15': + resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.15': + resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.15': + resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.15': + resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.15': + resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.15': + resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.15': + resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.15': + resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.15': + resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.15': + resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.15': + resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.15': + resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.15': + resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.15': + resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.15': + resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.10': + resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.6.6': + resolution: {integrity: sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==} + + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} + peerDependencies: + msw: ^2.4.9 || ^3.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-simple-import-sort@14.0.0: + resolution: {integrity: sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.13.0: + resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + magic-string@1.4.5: + resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + oxfmt@0.73.0: + resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@astrojs/compiler-rs': ^0.4.0 || ^0.5.0 + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + '@astrojs/compiler-rs': + optional: true + svelte: + optional: true + vite-plus: + optional: true + + oxlint@1.88.0: + resolution: {integrity: sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + rolldown@1.2.15: + resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.3.5: + resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 || ^0.8.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@eslint-community/eslint-utils@4.10.1(eslint@10.13.0)': + dependencies: + eslint: 10.13.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@oxc-project/types@0.155.0': {} + + '@oxfmt/binding-android-arm-eabi@0.73.0': + optional: true + + '@oxfmt/binding-android-arm64@0.73.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.73.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.73.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.73.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.73.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.73.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.73.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.73.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.73.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.73.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.73.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.73.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.73.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.73.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.73.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.73.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.73.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.73.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.88.0': + optional: true + + '@oxlint/binding-android-arm64@1.88.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.88.0': + optional: true + + '@oxlint/binding-darwin-x64@1.88.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.88.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.88.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.88.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.88.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.88.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.88.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.88.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.88.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.88.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.88.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.88.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.88.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.88.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.88.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.88.0': + optional: true + + '@qauth-labs/plugin-sdk@0.2.0-rc.1(zod@4.6.7)': + dependencies: + zod: 4.6.7 + + '@rolldown/binding-android-arm-eabi@1.2.15': + optional: true + + '@rolldown/binding-android-arm64@1.2.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.15': + optional: true + + '@rolldown/binding-darwin-x64@1.2.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.15': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.15': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.10': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.6.6': + dependencies: + undici-types: 8.9.0 + + '@vitest/mocker@5.0.5(vite@8.3.5(@types/node@26.6.6))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.5 + estree-walker: 3.0.3 + magic-string: 1.4.5 + optionalDependencies: + vite: 8.3.5(@types/node@26.6.6) + + '@vitest/spy@5.0.5': {} + + acorn-jsx@5.3.2(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + + acorn@8.19.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + assertion-error@2.0.1: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + chai@6.3.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + es-module-lexer@2.3.2: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-simple-import-sort@14.0.0(eslint@10.13.0): + dependencies: + eslint: 10.13.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.10 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.13.0: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.10 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.10 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + ignore@5.3.2: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + magic-string@1.4.5: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + natural-compare@1.4.0: {} + + obug@3.0.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + oxfmt@0.73.0: + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.73.0 + '@oxfmt/binding-android-arm64': 0.73.0 + '@oxfmt/binding-darwin-arm64': 0.73.0 + '@oxfmt/binding-darwin-x64': 0.73.0 + '@oxfmt/binding-freebsd-x64': 0.73.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.73.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.73.0 + '@oxfmt/binding-linux-arm64-gnu': 0.73.0 + '@oxfmt/binding-linux-arm64-musl': 0.73.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.73.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.73.0 + '@oxfmt/binding-linux-riscv64-musl': 0.73.0 + '@oxfmt/binding-linux-s390x-gnu': 0.73.0 + '@oxfmt/binding-linux-x64-gnu': 0.73.0 + '@oxfmt/binding-linux-x64-musl': 0.73.0 + '@oxfmt/binding-openharmony-arm64': 0.73.0 + '@oxfmt/binding-win32-arm64-msvc': 0.73.0 + '@oxfmt/binding-win32-ia32-msvc': 0.73.0 + '@oxfmt/binding-win32-x64-msvc': 0.73.0 + + oxlint@1.88.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.88.0 + '@oxlint/binding-android-arm64': 1.88.0 + '@oxlint/binding-darwin-arm64': 1.88.0 + '@oxlint/binding-darwin-x64': 1.88.0 + '@oxlint/binding-freebsd-x64': 1.88.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.88.0 + '@oxlint/binding-linux-arm-musleabihf': 1.88.0 + '@oxlint/binding-linux-arm64-gnu': 1.88.0 + '@oxlint/binding-linux-arm64-musl': 1.88.0 + '@oxlint/binding-linux-ppc64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-musl': 1.88.0 + '@oxlint/binding-linux-s390x-gnu': 1.88.0 + '@oxlint/binding-linux-x64-gnu': 1.88.0 + '@oxlint/binding-linux-x64-musl': 1.88.0 + '@oxlint/binding-openharmony-arm64': 1.88.0 + '@oxlint/binding-win32-arm64-msvc': 1.88.0 + '@oxlint/binding-win32-ia32-msvc': 1.88.0 + '@oxlint/binding-win32-x64-msvc': 1.88.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + rolldown@1.2.15: + dependencies: + '@oxc-project/types': 0.155.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.15 + '@rolldown/binding-android-arm64': 1.2.15 + '@rolldown/binding-darwin-arm64': 1.2.15 + '@rolldown/binding-darwin-x64': 1.2.15 + '@rolldown/binding-freebsd-x64': 1.2.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.15 + '@rolldown/binding-linux-arm64-gnu': 1.2.15 + '@rolldown/binding-linux-arm64-musl': 1.2.15 + '@rolldown/binding-linux-ppc64-gnu': 1.2.15 + '@rolldown/binding-linux-s390x-gnu': 1.2.15 + '@rolldown/binding-linux-x64-gnu': 1.2.15 + '@rolldown/binding-linux-x64-musl': 1.2.15 + '@rolldown/binding-openharmony-arm64': 1.2.15 + '@rolldown/binding-win32-arm64-msvc': 1.2.15 + '@rolldown/binding-win32-x64-msvc': 1.2.15 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.2: {} + + std-env@4.3.0: {} + + tinybench@6.2.2: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@2.2.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@6.0.3: {} + + undici-types@8.9.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.3.5(@types/node@26.6.6): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.32 + rolldown: 1.2.15 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.6 + fsevents: 2.3.3 + + vitest@5.0.5(@types/node@26.6.6)(vite@8.3.5(@types/node@26.6.6)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.5(vite@8.3.5(@types/node@26.6.6)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.5 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.5(@types/node@26.6.6) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 26.6.6 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@3.2.1: {} + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} + + zod@4.6.7: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..1d95b50 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,17 @@ +# pnpm settings for this repository, which is one package (no workspace +# packages). The same rules as QAuth's own pnpm-workspace.yaml. + +# Supply-chain cooldown: a version is resolved only once it has been on the +# registry for 7 days (10080 minutes), long enough for a hijacked release to be +# reported and pulled. A version may be taken sooner only when it is listed +# below by exact version, with why and when it matures; remove each once it +# matures. +minimumReleaseAge: 10080 +minimumReleaseAgeExclude: + # QAuth's plugin SDK, provenance-signed by qauth-labs/qauth's release-npm + # workflow at tag v0.2.0-rc.1: the contract this plugin is written against. + # Published 2026-11-05T09:17:29Z: remove after 2026-11-12T09:18Z. + - '@qauth-labs/plugin-sdk@0.2.0-rc.1' + +# Never auto-install before a script runs; warn when node_modules is stale. +verifyDepsBeforeRun: warn diff --git a/test/method.test.ts b/test/method.test.ts index 3c9cac9..9558ee7 100644 --- a/test/method.test.ts +++ b/test/method.test.ts @@ -13,8 +13,8 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { configSchema, type GitHubConfig } from '../src/config.js'; import { authMethod, type GitHubTransaction } from '../src/index.js'; -import { CONFIG, FakeGitHub } from './fake-github.js'; import { type FakeContext, fakeContext, REALM_ORIGIN } from './fake-context.js'; +import { CONFIG, FakeGitHub } from './fake-github.js'; const callback = authMethod.routes[0] as AuthMethodCallbackRoute; From 7a9877e77c0b363938e30340d3167e3fb98b5d76 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:47:12 +0100 Subject: [PATCH 08/10] fix(e2e): give the server the email settings it needs to boot The released server refuses to boot in production without EMAIL_FROM_ADDRESS; the first CI run stopped there with a ZodError. The deployment now sets it, and EMAIL_BASE_URL, as QAuth's upgrade test does. run.sh stops waiting for a server that exited during its boot instead of waiting out the two minutes, and packs the plugin and runs the suite with pnpm. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- e2e/compose.yml | 3 +++ e2e/run.sh | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/e2e/compose.yml b/e2e/compose.yml index b773b77..9dc751f 100644 --- a/e2e/compose.yml +++ b/e2e/compose.yml @@ -34,6 +34,9 @@ x-server: &server SESSION_COOKIE_SECRET: ${SESSION_COOKIE_SECRET:?run e2e/run.sh} # The suite talks to the server over plain http on localhost. SESSION_COOKIE_SECURE: 'false' + # Required at boot; the suite sends no mail (the mock email provider). + EMAIL_FROM_ADDRESS: noreply@qauth.test + EMAIL_BASE_URL: http://localhost:3000 # The first boot seeds the master realm with sign-up allowed, so a first # sign-in through GitHub creates the account. ALLOW_SIGN_UP: 'true' diff --git a/e2e/run.sh b/e2e/run.sh index 75bd463..386f571 100644 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -3,9 +3,9 @@ # installed the way an operator installs it, against a mock GitHub Enterprise # Server, driven over HTTP by e2e/github.e2e.test.ts. # -# Needs docker (with compose), openssl and node. Run it from anywhere: +# Needs docker (with compose), openssl, node and pnpm. Run it from anywhere: # -# bash e2e/run.sh +# pnpm run e2e # # It packs the plugin as npm would publish it, makes throwaway secrets and a # throwaway CA in e2e/.work/, builds the image (e2e/Dockerfile), starts the @@ -51,7 +51,7 @@ export MOCK_GITHUB_CLIENT_ID MOCK_GITHUB_CLIENT_SECRET chmod 644 "$here/egress-allowlist.json" # The plugin as npm would receive it. -(cd "$root" && npm pack --silent --pack-destination "$work/pack" > /dev/null) +(cd "$root" && pnpm run build > /dev/null && pnpm pack --pack-destination "$work/pack" > /dev/null) cp "$work"/pack/*.tgz "$work/plugin.tgz" finish() { @@ -67,11 +67,16 @@ finish() { trap finish EXIT wait_ready() { - local url="$1" + local url="$1" service="$2" for _ in $(seq 1 120); do if curl --silent --fail "$url" 2> /dev/null | grep -q '"status":"ok"'; then return 0 fi + # A server that stopped at boot will not become ready: stop waiting. + if [ -n "$("${compose[@]}" --profile plugin-api-off ps --quiet --status exited "$service")" ]; then + echo "$service stopped during its boot" >&2 + return 1 + fi sleep 1 done echo "$url did not become ready" >&2 @@ -80,13 +85,13 @@ wait_ready() { "${compose[@]}" build auth-server "${compose[@]}" up --detach auth-server -wait_ready http://localhost:3000/ready +wait_ready http://localhost:3000/ready auth-server # The second server boots on the database the first one seeded. "${compose[@]}" --profile plugin-api-off up --detach --no-build auth-server-plugin-api-off -wait_ready http://localhost:3010/ready +wait_ready http://localhost:3010/ready auth-server-plugin-api-off cd "$root" E2E_COMPOSE_FILE="$here/compose.yml" \ E2E_WORK="$work" \ NODE_EXTRA_CA_CERTS="$work/certs/ca.crt" \ - npx vitest run --config e2e/vitest.config.ts + pnpm exec vitest run --config e2e/vitest.config.ts From 5c04aa13d8d346ada1c3fbf28ec19ff0c7daab86 Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 12:53:34 +0100 Subject: [PATCH 09/10] build: sort imports with Oxfmt, not an ESLint plugin eslint-plugin-simple-import-sort needed ESLint itself as a peer, so the tree carried ESLint for one rule nothing else used. Oxfmt's sortImports now orders imports: `oxfmt --check` fails on an unsorted import list and `oxfmt` fixes it. No ESLint package is left in the lockfile. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn52bsHEfBLq12Xdx7d3 --- .oxfmtrc.json | 2 + .oxlintrc.json | 15 +- package.json | 1 - pnpm-lock.yaml | 609 ------------------------------------------------- 4 files changed, 8 insertions(+), 619 deletions(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index cec16af..c853781 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -10,6 +10,8 @@ "arrowParens": "always", "endOfLine": "lf", "sortPackageJson": false, + // The order of imports, checked by `oxfmt --check` and fixed by `oxfmt`. + "sortImports": true, // Gitignore-style; .gitignore applies too. "ignorePatterns": ["node_modules", "dist", "coverage", "*.log", "pnpm-lock.yaml", "e2e/.work"] } diff --git a/.oxlintrc.json b/.oxlintrc.json index 788ed04..e668d75 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,12 +1,12 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", // QAuth's own rule set (qauth-labs/qauth's .oxlintrc.json): @eslint/js and - // typescript-eslint `recommended` as Nx configures them, and - // simple-import-sort, less the Nx module boundaries and QAuth's key custody - // guard, which concern QAuth's own workspace. Oxlint's default category is - // off, so nothing else runs. Formatting is Oxfmt's job (.oxfmtrc.json). + // typescript-eslint `recommended` as Nx configures them, less the Nx module + // boundaries and QAuth's key custody guard, which concern QAuth's own + // workspace. Oxlint's default category is off, so nothing else runs. + // Formatting and the order of imports are Oxfmt's job (.oxfmtrc.json): no + // ESLint plugin, so no ESLint in the tree. "plugins": ["eslint", "typescript"], - "jsPlugins": ["eslint-plugin-simple-import-sort"], "categories": { "correctness": "off" }, @@ -86,10 +86,7 @@ "typescript/no-wrapper-object-types": "error", "typescript/prefer-as-const": "error", "typescript/prefer-namespace-keyword": "error", - "typescript/triple-slash-reference": "error", - - "simple-import-sort/imports": "error", - "simple-import-sort/exports": "error" + "typescript/triple-slash-reference": "error" }, "overrides": [ { diff --git a/package.json b/package.json index a6fc142..f916537 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ }, "devDependencies": { "@types/node": "^26.6.5", - "eslint-plugin-simple-import-sort": "^14.0.0", "oxfmt": "0.73.0", "oxlint": "1.88.0", "typescript": "~6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc71138..57fb0ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,9 +173,6 @@ importers: '@types/node': specifier: ^26.6.5 version: 26.6.6 - eslint-plugin-simple-import-sort: - specifier: ^14.0.0 - version: 14.0.0(eslint@10.13.0) oxfmt: specifier: 0.73.0 version: 0.73.0 @@ -194,62 +191,6 @@ importers: packages: - '@cacheable/memory@2.2.0': - resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} - - '@cacheable/utils@2.5.0': - resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} - - '@eslint-community/eslint-utils@4.10.1': - resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.23.5': - resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.7.0': - resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.2.1': - resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/object-schema@3.0.5': - resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.3': - resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -260,15 +201,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@keyv/bigmap@1.3.1': - resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} - engines: {node: '>= 18'} - peerDependencies: - keyv: ^5.6.0 - - '@keyv/serialize@1.1.1': - resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@oxc-project/types@0.155.0': resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} @@ -627,15 +559,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@1.0.10': resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@26.6.6': resolution: {integrity: sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==} @@ -653,54 +579,14 @@ packages: '@vitest/spy@5.0.5': resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.19.0: - resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - brace-expansion@5.0.12: - resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} - engines: {node: 20 || >=22} - - cacheable@2.5.0: - resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} - chai@6.3.0: resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -708,73 +594,13 @@ packages: es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-plugin-simple-import-sort@14.0.0: - resolution: {integrity: sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==} - peerDependencies: - eslint: '>=5.0.0' - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.13.0: - resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -784,70 +610,11 @@ packages: picomatch: optional: true - file-entry-cache@11.1.5: - resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@6.1.23: - resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} - - flatted@3.4.4: - resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - hashery@1.5.1: - resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} - engines: {node: '>=20'} - - hookified@1.15.1: - resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} - - hookified@2.2.0: - resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@5.6.0: - resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -922,36 +689,18 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - magic-string@1.4.5: resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} - minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} - engines: {node: 18 || 20 || >=22} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.20: resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - obug@3.0.0: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - oxfmt@0.73.0: resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -981,22 +730,6 @@ packages: vite-plus: optional: true - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1008,31 +741,11 @@ packages: resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} engines: {node: ^10 || ^12 || >=14} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - qified@0.10.1: - resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} - engines: {node: '>=20'} - rolldown@1.2.15: resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -1056,10 +769,6 @@ packages: resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} engines: {node: ^20.0.0 || >=22.0.0} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -1068,9 +777,6 @@ packages: undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite@8.3.5: resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1155,87 +861,16 @@ packages: jsdom: optional: true - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - why-is-node-running@3.2.1: resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} engines: {node: '>=20.11'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - zod@4.6.7: resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} snapshots: - '@cacheable/memory@2.2.0': - dependencies: - '@cacheable/utils': 2.5.0 - '@keyv/bigmap': 1.3.1(keyv@5.6.0) - hookified: 1.15.1 - keyv: 5.6.0 - - '@cacheable/utils@2.5.0': - dependencies: - hashery: 1.5.1 - keyv: 5.6.0 - - '@eslint-community/eslint-utils@4.10.1(eslint@10.13.0)': - dependencies: - eslint: 10.13.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.23.5': - dependencies: - '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.6 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.7.0': - dependencies: - '@eslint/core': 1.2.1 - - '@eslint/core@1.2.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/object-schema@3.0.5': {} - - '@eslint/plugin-kit@0.7.3': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.6.0': {} @@ -1245,14 +880,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 - '@keyv/bigmap@1.3.1(keyv@5.6.0)': - dependencies: - hashery: 1.5.1 - hookified: 1.15.1 - keyv: 5.6.0 - - '@keyv/serialize@1.1.1': {} - '@oxc-project/types@0.155.0': {} '@oxfmt/binding-android-arm-eabi@0.73.0': @@ -1427,12 +1054,8 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/esrecurse@4.3.1': {} - '@types/estree@1.0.10': {} - '@types/json-schema@7.0.15': {} - '@types/node@26.6.6': dependencies: undici-types: 8.9.0 @@ -1448,196 +1071,27 @@ snapshots: '@vitest/spy@5.0.5': {} - acorn-jsx@5.3.2(acorn@8.19.0): - dependencies: - acorn: 8.19.0 - - acorn@8.19.0: {} - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - assertion-error@2.0.1: {} - balanced-match@4.0.4: {} - - brace-expansion@5.0.12: - dependencies: - balanced-match: 4.0.4 - - cacheable@2.5.0: - dependencies: - '@cacheable/memory': 2.2.0 - '@cacheable/utils': 2.5.0 - hookified: 1.15.1 - keyv: 5.6.0 - qified: 0.10.1 - chai@6.3.0: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - detect-libc@2.1.2: {} es-module-lexer@2.3.2: {} - escape-string-regexp@4.0.0: {} - - eslint-plugin-simple-import-sort@14.0.0(eslint@10.13.0): - dependencies: - eslint: 10.13.0 - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.10 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.13.0: - dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.7.0 - '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.3 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.10 - ajv: 6.15.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 11.1.5 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.6 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@11.2.0: - dependencies: - acorn: 8.19.0 - acorn-jsx: 5.3.2(acorn@8.19.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.10 - esutils@2.0.3: {} - expect-type@1.4.0: {} - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 - file-entry-cache@11.1.5: - dependencies: - flat-cache: 6.1.23 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@6.1.23: - dependencies: - cacheable: 2.5.0 - flatted: 3.4.4 - hookified: 1.15.1 - - flatted@3.4.4: {} - fsevents@2.3.3: optional: true - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - hashery@1.5.1: - dependencies: - hookified: 1.15.1 - - hookified@1.15.1: {} - - hookified@2.2.0: {} - - ignore@5.3.2: {} - - imurmurhash@0.1.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@5.6.0: - dependencies: - '@keyv/serialize': 1.1.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - lightningcss-android-arm64@1.33.0: optional: true @@ -1687,35 +1141,14 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - magic-string@1.4.5: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - minimatch@10.2.6: - dependencies: - brace-expansion: 5.0.12 - - ms@2.1.3: {} - nanoid@3.3.20: {} - natural-compare@1.4.0: {} - obug@3.0.0: {} - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - oxfmt@0.73.0: dependencies: tinypool: 2.2.0 @@ -1762,18 +1195,6 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.88.0 '@oxlint/binding-win32-x64-msvc': 1.88.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -1784,14 +1205,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 - prelude-ls@1.2.1: {} - - punycode@2.3.1: {} - - qified@0.10.1: - dependencies: - hookified: 2.2.0 - rolldown@1.2.15: dependencies: '@oxc-project/types': 0.155.0 @@ -1813,12 +1226,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.15 '@rolldown/binding-win32-x64-msvc': 1.2.15 - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - source-map-js@1.2.2: {} std-env@4.3.0: {} @@ -1834,18 +1241,10 @@ snapshots: tinypool@2.2.0: {} - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - typescript@6.0.3: {} undici-types@8.9.0: {} - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - vite@8.3.5(@types/node@26.6.6): dependencies: lightningcss: 1.33.0 @@ -1878,14 +1277,6 @@ snapshots: transitivePeerDependencies: - msw - which@2.0.2: - dependencies: - isexe: 2.0.0 - why-is-node-running@3.2.1: {} - word-wrap@1.2.5: {} - - yocto-queue@0.1.0: {} - zod@4.6.7: {} From 6f69ead8501ab848e9fc3926c19176c90ab0ae4b Mon Sep 17 00:00:00 2001 From: Muhammed Taha Ayan Date: Thu, 5 Nov 2026 13:19:01 +0100 Subject: [PATCH 10/10] docs: install the plugin into the image from a lockfile The README's recipe ran npm install in a fresh project, so the ranges of the plugin's dependencies resolved again at every build. It now writes a lockfile once with npm install --package-lock-only, has the operator read and commit it, and installs with npm ci, as QAuth's own image docs now do. The e2e Dockerfile says why it installs the freshly packed tarball instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXFn4xb9VzHZMjiUTi7Tof --- README.md | 22 +++++++++++++++++----- e2e/Dockerfile | 3 ++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5381c0d..b75f5b8 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,18 @@ The image is a self-contained tree under `/app`. Install the plugin into a separate stage, nested, so it brings its own copy of the plugin SDK, and copy it in. `--omit=peer` makes it use the server's own `zod`; `--ignore-scripts` runs no install script. Pin the server image by digest and the plugin by -version: +version, and install from a lockfile you have read, so every build gets the +same dependency tree. Write the lockfile once, in a `plugins/` directory of +your build context, without installing anything: + +```bash +mkdir plugins && cd plugins +npm init --yes > /dev/null +npm install --package-lock-only --save-exact --omit=dev --omit=peer \ + --install-strategy=nested --ignore-scripts @qauth-labs/plugin-github@0.1.0-rc.0 +``` + +Read `package-lock.json` and commit both files. Then: ```dockerfile ARG QAUTH_IMAGE=ghcr.io/qauth-labs/qauth/auth-server:0.2.0-rc.1@sha256: @@ -118,9 +129,8 @@ ARG QAUTH_IMAGE=ghcr.io/qauth-labs/qauth/auth-server:0.2.0-rc.1@sha256: # The build stage writes the files the server will run: pin it by digest too. FROM node:26-alpine@sha256: AS plugin WORKDIR /plugin -RUN npm init --yes > /dev/null \ - && npm install --omit=dev --omit=peer --install-strategy=nested --ignore-scripts \ - @qauth-labs/plugin-github@0.1.0-rc.0 +COPY plugins/package.json plugins/package-lock.json ./ +RUN npm ci --omit=dev --omit=peer --install-strategy=nested --ignore-scripts FROM ${QAUTH_IMAGE} COPY --from=plugin --chown=nodejs:nodejs \ @@ -128,7 +138,9 @@ COPY --from=plugin --chown=nodejs:nodejs \ /app/node_modules/@qauth-labs/plugin-github ``` -Rebuild it at every QAuth upgrade. To know the package was built by this +Rebuild it at every QAuth upgrade, with the lockfile moved to the plugin +version made for that release by the same `npm install --package-lock-only`. +To know the package was built by this repository's release workflow, read the version's provenance on npmjs.com, or the attestation at `npm view @qauth-labs/plugin-github@ dist.attestations.url`, and diff --git a/e2e/Dockerfile b/e2e/Dockerfile index 28203be..685a582 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -5,7 +5,8 @@ # # The plugin is installed nested, so it brings its own copy of the plugin SDK, # without its peer dependencies, so it uses the server's zod, and without -# install scripts. +# install scripts. An operator installs a released version from a lockfile +# with `npm ci`; this test installs the tarball it has just packed. ARG QAUTH_IMAGE=ghcr.io/qauth-labs/qauth/auth-server:0.2.0-rc.1@sha256:a4b199c5b1e896346d09c348f0a6cb055ab4f64bf82e0654740b0aa2631e5e8c FROM node:26-alpine@sha256:c6ad9e9c1d80f6eb607c26b4b0a132ff7a2344fad1c78ea3baa2a7d4c8de6d94 AS plugin