diff --git a/CLAUDE.md b/CLAUDE.md index 789ec881ff35..b2ff91567b42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,7 +101,7 @@ test("(multi-file test) my feature", async () => { - Use `tempDir` from `"harness"` to create a temporary directory. **Do not** use `tmpdirSync` or `fs.mkdtempSync` to create temporary directories. - When spawning processes, tests should expect(stdout).toBe(...) BEFORE expect(exitCode).toBe(0). This gives you a more useful error message on test failure. - Keep tests fast: budget roughly 1s per test and 10s per file. Debug+ASAN builds run 10-100x slower than release, so a 1s local test can take a minute in CI. Use `test.concurrent` for independent subprocess-spawning tests. -- Never contact the public internet (registry.npmjs.org, github.com, CDNs). Use `VerdaccioRegistry` from `"harness"` for package installs and a local `Bun.serve({ port: 0 })` for HTTP. +- Never contact the public internet (registry.npmjs.org, github.com, CDNs). Use `TestRegistry` from `"registry"` (`test/packages/registry`) for package installs and a local `Bun.serve({ port: 0 })` for HTTP. - `setDefaultTimeout` is a ceiling, not a target. Leave the default and pass a per-test timeout only for the rare outlier; a 5-minute file default multiplies across retries when one test hangs. - Leak tests branch their RSS threshold on `isASAN`/`isDebug` and keep the bound well below what the unfixed leak produces. An un-branched absolute delta flakes under ASAN quarantine and GC jitter. - **CRITICAL**: Do not write flaky tests. Do not use `setTimeout` or `await sleep(N)` to wait for a condition; poll with a deadline or `await` the event itself. You are not testing the TIME PASSING, you are testing the CONDITION. diff --git a/test/bun.lock b/test/bun.lock index 0dcc5c65ac03..50fc5967e9c1 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -26,7 +26,6 @@ "@swc/core": "1.3.38", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", - "@verdaccio/config": "6.0.0-6-next.76", "@vitest/coverage-v8": "4.1.9", "acorn": "8.15.0", "ansi-regex": "6.1.0", @@ -102,7 +101,6 @@ "unzipper": "0.12.3", "uuid": "11.1.0", "v8-heapsnapshot": "1.3.1", - "verdaccio": "6.0.0", "vitest": "4.1.9", "webpack": "5.88.0", "webpack-cli": "4.7.2", @@ -209,8 +207,6 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="], - "@cypress/request": ["@cypress/request@3.0.5", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.0", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "6.13.0", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA=="], - "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], @@ -699,8 +695,6 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/methods": ["@types/methods@1.1.4", "", {}, "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ=="], @@ -745,42 +739,6 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@verdaccio/auth": ["@verdaccio/auth@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.1", "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/loaders": "8.0.0-next-8.1", "@verdaccio/logger": "8.0.0-next-8.1", "@verdaccio/signature": "8.0.0-next-8.0", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "lodash": "4.17.21", "verdaccio-htpasswd": "13.0.0-next-8.1" } }, "sha512-sPmHdnYuRSMgABCsTJEfz8tb/smONsWVg0g4KK2QycyYZ/A+RwZLV1JLiQb4wzu9zvS0HSloqWqkWlyNHW3mtw=="], - - "@verdaccio/commons-api": ["@verdaccio/commons-api@10.2.0", "", { "dependencies": { "http-errors": "2.0.0", "http-status-codes": "2.2.0" } }, "sha512-F/YZANu4DmpcEV0jronzI7v2fGVWkQ5Mwi+bVmV+ACJ+EzR0c9Jbhtbe5QyLUuzR97t8R5E/Xe53O0cc2LukdQ=="], - - "@verdaccio/config": ["@verdaccio/config@6.0.0-6-next.76", "", { "dependencies": { "@verdaccio/core": "6.0.0-6-next.76", "@verdaccio/utils": "6.0.0-6-next.44", "debug": "4.3.4", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "3.1.2", "yup": "0.32.11" } }, "sha512-iWmmi+8pDedpwbRw43K19+Ky+DBYwcQ3L9hQNkG4KrhGdJsy7it6bpcjmxrh4xX39jQ00bqyq6Hb4cCGl7zlRQ=="], - - "@verdaccio/core": ["@verdaccio/core@6.0.0-6-next.76", "", { "dependencies": { "ajv": "8.12.0", "core-js": "3.30.2", "http-errors": "2.0.0", "http-status-codes": "2.2.0", "process-warning": "1.0.0", "semver": "7.5.4" } }, "sha512-1Jr4Ft61GWAKRlv8joGNa9e/zyaqm7FTP8qkKNl84LEjpmUomn5UrXYbiq4d1DESd7SVVQEfpRmYp1wnPniSHQ=="], - - "@verdaccio/file-locking": ["@verdaccio/file-locking@10.3.1", "", { "dependencies": { "lockfile": "1.0.4" } }, "sha512-oqYLfv3Yg3mAgw9qhASBpjD50osj2AX4IwbkUtyuhhKGyoFU9eZdrbeW6tpnqUnj6yBMtAPm2eGD4BwQuX400g=="], - - "@verdaccio/loaders": ["@verdaccio/loaders@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/logger": "8.0.0-next-8.1", "debug": "4.3.7", "lodash": "4.17.21" } }, "sha512-mqGCUBs862g8mICZwX8CG92p1EZ1Un0DJ2DB7+iVu2TYaEeKoHoIdafabVdiYrbOjLcAOOBrMKE1Wnn14eLxpA=="], - - "@verdaccio/local-storage-legacy": ["@verdaccio/local-storage-legacy@11.0.2", "", { "dependencies": { "@verdaccio/commons-api": "10.2.0", "@verdaccio/file-locking": "10.3.1", "@verdaccio/streams": "10.2.1", "async": "3.2.4", "debug": "4.3.4", "lodash": "4.17.21", "lowdb": "1.0.0", "mkdirp": "1.0.4" } }, "sha512-7AXG7qlcVFmF+Nue2oKaraprGRtaBvrQIOvc/E89+7hAe399V01KnZI6E/ET56u7U9fq0MSlp92HBcdotlpUXg=="], - - "@verdaccio/logger": ["@verdaccio/logger@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/logger-commons": "8.0.0-next-8.1", "pino": "8.17.2" } }, "sha512-w5kR0/umQkfH2F4PK5Fz9T6z3xz+twewawKLPTUfAgrVAOiWxcikGhhcHWhSGiJ0lPqIa+T0VYuLWMeVeDirGw=="], - - "@verdaccio/logger-commons": ["@verdaccio/logger-commons@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/logger-prettify": "8.0.0-next-8.0", "colorette": "2.0.20", "debug": "4.3.7" } }, "sha512-jCge//RT4uaK7MarhpzcJeJ5Uvtu/DbJ1wvJQyGiFe+9AvxDGm3EUFXvawLFZ0lzYhmLt1nvm7kevcc3vOm2ZQ=="], - - "@verdaccio/logger-prettify": ["@verdaccio/logger-prettify@8.0.0-next-8.0", "", { "dependencies": { "colorette": "2.0.20", "dayjs": "1.11.13", "lodash": "4.17.21", "pino-abstract-transport": "1.1.0", "sonic-boom": "3.8.0" } }, "sha512-7mAFHZF2NPTubrOXYp2+fbMjRW5MMWXMeS3LcpupMAn5uPp6jkKEM8NC4IVJEevC5Ph4vPVZqpoPDpgXHEaV3Q=="], - - "@verdaccio/middleware": ["@verdaccio/middleware@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.1", "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/url": "13.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "express": "4.21.0", "express-rate-limit": "5.5.1", "lodash": "4.17.21", "lru-cache": "7.18.3", "mime": "2.6.0" } }, "sha512-GpAdJYky1WmOERpxPoCkVSwTTJIsVAjqf2a2uQNvi7R3UZhs059JKhWcZjJMVCGV0uz9xgQvtb3DEuYGHqyaOg=="], - - "@verdaccio/search-indexer": ["@verdaccio/search-indexer@8.0.0-next-8.0", "", {}, "sha512-VS9axVt8XAueiPceVCgaj9nlvYj5s/T4MkAILSf2rVZeFFOMUyxU3mddUCajSHzL+YpqCuzLLL9865sRRzOJ9w=="], - - "@verdaccio/signature": ["@verdaccio/signature@8.0.0-next-8.0", "", { "dependencies": { "debug": "4.3.7", "jsonwebtoken": "9.0.2" } }, "sha512-klcc2UlCvQxXDV65Qewo2rZOfv7S1y8NekS/8uurSaCTjU35T+fz+Pbqz1S9XK9oQlMp4vCQ7w3iMPWQbvphEQ=="], - - "@verdaccio/streams": ["@verdaccio/streams@10.2.1", "", {}, "sha512-OojIG/f7UYKxC4dYX8x5ax8QhRx1b8OYUAMz82rUottCuzrssX/4nn5QE7Ank0DUSX3C9l/HPthc4d9uKRJqJQ=="], - - "@verdaccio/tarball": ["@verdaccio/tarball@13.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/url": "13.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "gunzip-maybe": "^1.4.2", "lodash": "4.17.21", "tar-stream": "^3.1.7" } }, "sha512-58uimU2Bqt9+s+9ixy7wK/nPCqbOXhhhr/MQjl+otIlsUhSeATndhFzEctz/W+4MhUDg0tUnE9HC2yeNHHAo1Q=="], - - "@verdaccio/ui-theme": ["@verdaccio/ui-theme@8.0.0-next-8.1", "", {}, "sha512-9PxV8+jE2Tr+iy9DQW/bzny4YqOlW0mCZ9ct6jhcUW4GdfzU//gY2fBN/DDtQVmfbTy8smuj4Enyv5f0wCsnYg=="], - - "@verdaccio/url": ["@verdaccio/url@13.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "debug": "4.3.7", "lodash": "4.17.21", "validator": "13.12.0" } }, "sha512-h6pkJf+YtogImKgOrmPP9UVG3p3gtb67gqkQU0bZnK+SEKQt6Rkek/QvtJ8MbmciagYS18bDhpI8DxqLHjDfZQ=="], - - "@verdaccio/utils": ["@verdaccio/utils@6.0.0-6-next.44", "", { "dependencies": { "@verdaccio/core": "6.0.0-6-next.76", "lodash": "4.17.21", "minimatch": "3.1.2", "semver": "7.5.4" } }, "sha512-fSuxes9lfQO8mBXfV8lAnfeWgM4iUbB4LFv8KgSSd7T3QP453MpS09zZmbJTdxgy8CyWeGvACIQvSXxkYoNxBA=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.9", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.9", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.9", "vitest": "4.1.9" }, "optionalPeers": ["@vitest/browser"] }, "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g=="], "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], @@ -843,8 +801,6 @@ "@zxing/text-encoding": ["@zxing/text-encoding@0.9.0", "", {}, "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA=="], - "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], - "abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -883,8 +839,6 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "apache-md5": ["apache-md5@1.1.8", "", {}, "sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA=="], - "app-root-path": ["app-root-path@3.1.0", "", {}, "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -901,10 +855,6 @@ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], - - "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA=="], @@ -913,8 +863,6 @@ "astro": ["astro@5.5.5", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.1", "@astrojs/telemetry": "3.2.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.0.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.0.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.5.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.2.3", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.3", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-fdnnK5dhWNIQT/cXzvaGs9il4T5noi4jafobdntbuNOrRxI1JnOxDfrtBadUo6cknCRCFhYrXh4VndCqj1a4Sg=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "async-cache": ["async-cache@1.1.0", "", { "dependencies": { "lru-cache": "^4.0.0" } }, "sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -929,22 +877,14 @@ "aws-cdk-lib": ["aws-cdk-lib@2.148.0", "", { "dependencies": { "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.3", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", "ignore": "^5.3.1", "jsonschema": "^1.4.1", "mime-types": "^2.1.35", "minimatch": "^3.1.2", "punycode": "^2.3.1", "semver": "^7.6.2", "table": "^6.8.2", "yaml": "1.10.2" }, "peerDependencies": { "constructs": "^10.0.0" } }, "sha512-Pa0pyIHlhnsqtMkPJS3tnptYhoOSNDOgoFurNB4Qfa0vnAkjYQ+JKQkR1tNNr8+UtO9jUfXRklQgjEqlFlrgBA=="], - "aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="], - - "aws4": ["aws4@1.13.0", "", {}, "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g=="], - "axios": ["axios@1.6.8", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "bare-events": ["bare-events@2.5.0", "", {}, "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A=="], - "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -955,10 +895,6 @@ "basic-auth-parser": ["basic-auth-parser@0.0.2-1", "", {}, "sha512-GFj8iVxo9onSU6BnnQvVwqvxh60UcSHJEDnIk3z4B6iOjsKSmqe+ibW0Rsz7YO7IE1HG3D3tqCNIidP46SZVdQ=="], - "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - - "bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="], @@ -975,8 +911,6 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserify-zlib": ["browserify-zlib@0.1.4", "", { "dependencies": { "pako": "~0.2.0" } }, "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ=="], - "browserslist": ["browserslist@4.23.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" } }, "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw=="], "bson": ["bson@6.7.0", "", {}, "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ=="], @@ -1009,8 +943,6 @@ "case": ["case@1.6.3", "", {}, "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ=="], - "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -1041,8 +973,6 @@ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "clipanion": ["clipanion@4.0.0-rc.3", "", { "dependencies": { "typanion": "^3.8.0" } }, "sha512-+rJOJMt2N6Oikgtfqmo/Duvme7uz3SIedL2b6ycgCztQMiTfr3aQh2DDyLHl+QUPClKMNpSg3gDJFvNQYIcq1g=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], @@ -1097,8 +1027,6 @@ "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], - "core-js": ["core-js@3.30.2", "", {}, "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg=="], - "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], @@ -1115,8 +1043,6 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="], - "data-uri-to-buffer": ["data-uri-to-buffer@3.0.1", "", {}, "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="], "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], @@ -1125,7 +1051,7 @@ "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "decimal.js": ["decimal.js@10.4.3", "", {}, "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="], @@ -1175,8 +1101,6 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="], - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -1253,20 +1177,14 @@ "express": ["express@4.18.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", "serve-static": "1.15.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ=="], - "express-rate-limit": ["express-rate-limit@5.5.1", "", {}, "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="], - "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], - "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -1319,8 +1237,6 @@ "foreground-child": ["foreground-child@3.2.1", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA=="], - "forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="], - "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], @@ -1349,8 +1265,6 @@ "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - "getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="], - "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -1371,12 +1285,8 @@ "graphql-scalars": ["graphql-scalars@1.24.0", "", { "dependencies": { "tslib": "^2.5.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-olbFN39m0XsHHESACUdd7jWU/lGxMMS1B7NZ8XqpqhKZrjBxzeGYAnQ4Ax//huYds771wb7gCznA+65QDuUa+g=="], - "gunzip-maybe": ["gunzip-maybe@1.4.2", "", { "dependencies": { "browserify-zlib": "^0.1.4", "is-deflate": "^1.0.0", "is-gzip": "^1.0.0", "peek-stream": "^1.1.0", "pumpify": "^1.3.3", "through2": "^2.0.3" }, "bin": { "gunzip-maybe": "bin.js" } }, "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw=="], - "h3": ["h3@1.15.1", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.3", "defu": "^6.1.4", "destr": "^2.0.3", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.5.4", "uncrypto": "^0.1.3" } }, "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA=="], - "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], - "happy-dom": ["happy-dom@17.0.3", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-1vWCwpeguN02wQF8kGeaj69FDX19bXKQXmyUKcE+O0WLY0uhS0RPTLCJR8Omy8hrjMHwV3dUJ24JUrK07aOA9Q=="], "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -1435,10 +1345,6 @@ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - "http-signature": ["http-signature@1.4.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", "sshpk": "^1.18.0" } }, "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg=="], - - "http-status-codes": ["http-status-codes@2.2.0", "", {}, "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="], - "http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -1485,8 +1391,6 @@ "is-core-module": ["is-core-module@2.13.1", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw=="], - "is-deflate": ["is-deflate@1.0.0", "", {}, "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ=="], - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -1497,8 +1401,6 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-gzip": ["is-gzip@1.0.0", "", {}, "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ=="], - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], @@ -1513,8 +1415,6 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], @@ -1523,8 +1423,6 @@ "is-typed-array": ["is-typed-array@1.1.13", "", { "dependencies": { "which-typed-array": "^1.1.14" } }, "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw=="], - "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], @@ -1535,8 +1433,6 @@ "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], @@ -1573,24 +1469,16 @@ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-ref-resolver": ["json-schema-ref-resolver@2.0.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], - "jsonschema": ["jsonschema@1.4.1", "", {}, "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], - "jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="], - "jssha": ["jssha@3.3.1", "", {}, "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ=="], "just-extend": ["just-extend@4.2.1", "", {}, "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg=="], @@ -1613,12 +1501,8 @@ "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "lockfile": ["lockfile@1.0.4", "", { "dependencies": { "signal-exit": "^3.0.2" } }, "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], @@ -1647,8 +1531,6 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lowdb": ["lowdb@1.0.0", "", { "dependencies": { "graceful-fs": "^4.1.3", "is-promise": "^2.1.0", "lodash": "4", "pify": "^3.0.0", "steno": "^0.4.1" } }, "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ=="], - "lru-cache": ["lru-cache@8.0.5", "", {}, "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1815,8 +1697,6 @@ "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], - "nanoclone": ["nanoclone@0.2.1", "", {}, "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -1829,8 +1709,6 @@ "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], - "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], - "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], "node-gyp": ["node-gyp@10.0.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^10.3.10", "graceful-fs": "^4.2.6", "make-fetch-happen": "^13.0.0", "nopt": "^7.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^4.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg=="], @@ -1931,10 +1809,6 @@ "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], - "peek-stream": ["peek-stream@1.1.3", "", { "dependencies": { "buffer-from": "^1.0.0", "duplexify": "^3.5.0", "through2": "^2.0.3" } }, "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA=="], - - "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], - "pg": ["pg@8.11.1", "", { "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.6.1", "pg-pool": "^3.6.1", "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ=="], "pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="], @@ -1957,8 +1831,6 @@ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], - "pino": ["pino@9.4.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^7.0.0", "process-warning": "^4.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w=="], "pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], @@ -1971,8 +1843,6 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "pkginfo": ["pkginfo@0.4.1", "", {}, "sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ=="], - "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], @@ -2007,8 +1877,6 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="], - "property-information": ["property-information@7.0.0", "", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="], "protobufjs": ["protobufjs@7.3.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg=="], @@ -2021,22 +1889,14 @@ "pseudomap": ["pseudomap@1.0.2", "", {}, "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="], - "psl": ["psl@1.9.0", "", {}, "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="], - "pump": ["pump@3.0.0", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww=="], - "pumpify": ["pumpify@1.5.1", "", { "dependencies": { "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" } }, "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "qs": ["qs@6.11.0", "", { "dependencies": { "side-channel": "^1.0.4" } }, "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q=="], - "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "queue-tick": ["queue-tick@1.0.1", "", {}, "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="], - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], @@ -2103,8 +1963,6 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - "resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], @@ -2245,8 +2103,6 @@ "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], - "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], - "ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], "st": ["st@3.0.0", "", { "dependencies": { "async-cache": "^1.1.0", "bl": "^5.0.0", "fd": "~0.0.3", "mime": "^2.5.2", "negotiator": "~0.6.2" }, "optionalDependencies": { "graceful-fs": "^4.2.3" }, "bin": { "st": "bin/server.js" } }, "sha512-UEUi8P8Y5GOewlJbE5vrhsaQRwmbNVMUr6PLxRZHH4Cwz8CkHhnBqlqGtE3egXQd+ceUwNxdOVjsC/IsgN2Pww=="], @@ -2257,16 +2113,12 @@ "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "steno": ["steno@0.4.4", "", { "dependencies": { "graceful-fs": "^4.1.3" } }, "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w=="], - "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], "stream-slice": ["stream-slice@0.1.2", "", {}, "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA=="], "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], - "streamx": ["streamx@2.20.1", "", { "dependencies": { "fast-fifo": "^1.3.2", "queue-tick": "^1.0.1", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA=="], - "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], "string-width": ["string-width@7.0.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw=="], @@ -2313,24 +2165,16 @@ "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], - "terser": ["terser@5.31.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg=="], "terser-webpack-plugin": ["terser-webpack-plugin@5.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", "terser": "^5.26.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w=="], - "text-decoder": ["text-decoder@1.2.0", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - - "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -2355,8 +2199,6 @@ "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], - "toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="], - "tough-cookie": ["tough-cookie@5.0.0", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q=="], "tr46": ["tr46@5.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g=="], @@ -2373,14 +2215,8 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo-stream": ["turbo-stream@2.2.0", "", {}, "sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g=="], - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - - "typanion": ["typanion@3.14.0", "", {}, "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug=="], - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], "type-fest": ["type-fest@4.38.0", "", {}, "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg=="], @@ -2395,8 +2231,6 @@ "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], - "uglify-js": ["uglify-js@3.18.0", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A=="], - "uid": ["uid@2.0.2", "", { "dependencies": { "@lukeed/csprng": "^1.0.0" } }, "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g=="], "ultrahtml": ["ultrahtml@1.5.3", "", {}, "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg=="], @@ -2433,8 +2267,6 @@ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unix-crypt-td-js": ["unix-crypt-td-js@1.1.4", "", {}, "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unstorage": ["unstorage@1.15.0", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.3", "h3": "^1.15.0", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.6", "ofetch": "^1.4.1", "ufo": "^1.5.4" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg=="], @@ -2445,8 +2277,6 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], - "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], @@ -2461,18 +2291,8 @@ "v8-heapsnapshot": ["v8-heapsnapshot@1.3.1", "", { "dependencies": { "@types/node": "^18.11.10", "@types/oboe": "^2.1.1", "oboe": "^2.1.5" } }, "sha512-mfRTctXCVczzUH2U4FmbBNXH2ikErxkHAlZOIpjLJeNijQRMeVkXJfvPJUMnlLueO2xUEleWK2TjhcxOnc2eRQ=="], - "validator": ["validator@13.12.0", "", {}, "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "verdaccio": ["verdaccio@6.0.0", "", { "dependencies": { "@cypress/request": "3.0.5", "@verdaccio/auth": "8.0.0-next-8.1", "@verdaccio/config": "8.0.0-next-8.1", "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/local-storage-legacy": "11.0.2", "@verdaccio/logger": "8.0.0-next-8.1", "@verdaccio/middleware": "8.0.0-next-8.1", "@verdaccio/search-indexer": "8.0.0-next-8.0", "@verdaccio/signature": "8.0.0-next-8.0", "@verdaccio/streams": "10.2.1", "@verdaccio/tarball": "13.0.0-next-8.1", "@verdaccio/ui-theme": "8.0.0-next-8.1", "@verdaccio/url": "13.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "JSONStream": "1.3.5", "async": "3.2.6", "clipanion": "4.0.0-rc.3", "compression": "1.7.4", "cors": "2.8.5", "debug": "4.3.7", "envinfo": "7.14.0", "express": "4.21.0", "express-rate-limit": "5.5.1", "fast-safe-stringify": "2.1.1", "handlebars": "4.7.8", "js-yaml": "4.1.0", "jsonwebtoken": "9.0.2", "kleur": "4.1.5", "lodash": "4.17.21", "lru-cache": "7.18.3", "mime": "3.0.0", "mkdirp": "1.0.4", "pkginfo": "0.4.1", "semver": "7.6.3", "validator": "13.12.0", "verdaccio-audit": "13.0.0-next-8.1", "verdaccio-htpasswd": "13.0.0-next-8.1" }, "bin": { "verdaccio": "bin/verdaccio" } }, "sha512-iGUIA992DIqjc4bge7L6NgpVDXHEvQ2ASBqBGr2AmZha+6g3+Rd5DV49ZR1z2MAvKmdyBwP9fOFmdh0PAsxa1Q=="], - - "verdaccio-audit": ["verdaccio-audit@13.0.0-next-8.1", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.1", "@verdaccio/core": "8.0.0-next-8.1", "express": "4.21.0", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" } }, "sha512-EEfUeC1kHuErtwF9FC670W+EXHhcl+iuigONkcprwRfkPxmdBs+Hx36745hgAMZ9SCqedNECaycnGF3tZ3VYfw=="], - - "verdaccio-htpasswd": ["verdaccio-htpasswd@13.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/file-locking": "13.0.0-next-8.0", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "core-js": "3.37.1", "debug": "4.3.7", "http-errors": "2.0.0", "unix-crypt-td-js": "1.1.4" } }, "sha512-BfvmO+ZdbwfttOwrdTPD6Bccr1ZfZ9Tk/9wpXamxdWB/XPWlk3FtyGsvqCmxsInRLPhQ/FSk9c3zRCGvICTFYg=="], - - "verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], @@ -2523,8 +2343,6 @@ "wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="], - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2565,8 +2383,6 @@ "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], - "yup": ["yup@0.32.11", "", { "dependencies": { "@babel/runtime": "^7.15.4", "@types/lodash": "^4.14.175", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "nanoclone": "^0.2.1", "property-expr": "^2.0.4", "toposort": "^2.0.2" } }, "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg=="], - "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], @@ -2579,8 +2395,6 @@ "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "@astrojs/telemetry/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "@azure/core-amqp/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], @@ -2595,12 +2409,6 @@ "@bundled-es-modules/cookie/cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="], - "@cypress/request/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], - - "@cypress/request/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], - - "@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "@fastify/ajv-compiler/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], @@ -2653,60 +2461,6 @@ "@testing-library/react/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], - "@verdaccio/auth/@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw=="], - - "@verdaccio/auth/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "@verdaccio/auth/@verdaccio/utils": ["@verdaccio/utils@7.0.1-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "lodash": "4.17.21", "minimatch": "7.4.6", "semver": "7.6.3" } }, "sha512-cyJdRrVa+8CS7UuIQb3K3IJFjMe64v38tYiBnohSmhRbX7dX9IT3jWbjrwkqWh4KeS1CS6BYENrGG1evJ2ggrQ=="], - - "@verdaccio/auth/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@verdaccio/core/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], - - "@verdaccio/loaders/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/local-storage-legacy/async": ["async@3.2.4", "", {}, "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="], - - "@verdaccio/local-storage-legacy/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - - "@verdaccio/logger/pino": ["pino@8.17.2", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", "thread-stream": "^2.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ=="], - - "@verdaccio/logger-commons/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "@verdaccio/logger-commons/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/logger-prettify/pino-abstract-transport": ["pino-abstract-transport@1.1.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA=="], - - "@verdaccio/logger-prettify/sonic-boom": ["sonic-boom@3.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA=="], - - "@verdaccio/middleware/@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw=="], - - "@verdaccio/middleware/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "@verdaccio/middleware/@verdaccio/utils": ["@verdaccio/utils@7.0.1-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "lodash": "4.17.21", "minimatch": "7.4.6", "semver": "7.6.3" } }, "sha512-cyJdRrVa+8CS7UuIQb3K3IJFjMe64v38tYiBnohSmhRbX7dX9IT3jWbjrwkqWh4KeS1CS6BYENrGG1evJ2ggrQ=="], - - "@verdaccio/middleware/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/middleware/express": ["express@4.21.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng=="], - - "@verdaccio/middleware/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "@verdaccio/signature/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/tarball/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "@verdaccio/tarball/@verdaccio/utils": ["@verdaccio/utils@7.0.1-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "lodash": "4.17.21", "minimatch": "7.4.6", "semver": "7.6.3" } }, "sha512-cyJdRrVa+8CS7UuIQb3K3IJFjMe64v38tYiBnohSmhRbX7dX9IT3jWbjrwkqWh4KeS1CS6BYENrGG1evJ2ggrQ=="], - - "@verdaccio/tarball/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/url/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "@verdaccio/url/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "@verdaccio/utils/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], - "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-keywords/ajv": ["ajv@6.12.6", "", { "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" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -2721,8 +2475,6 @@ "args/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "astro/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "astro/es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], "astro/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], @@ -2745,8 +2497,6 @@ "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "browserify-zlib/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], - "cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -2777,14 +2527,10 @@ "data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "ecc-jsbn/jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="], - "engine.io/@types/cookie": ["@types/cookie@0.4.1", "", {}, "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="], "engine.io/cookie": ["cookie@0.4.2", "", {}, "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="], @@ -2821,12 +2567,8 @@ "glob/minimatch": ["minimatch@9.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw=="], - "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], - "http-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -2849,8 +2591,6 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "micromark/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -2879,8 +2619,6 @@ "nise/path-to-regexp": ["path-to-regexp@1.8.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA=="], - "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -2889,8 +2627,6 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="], - "pino-pretty/secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], @@ -2901,12 +2637,6 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "proxy/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "pumpify/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="], - - "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="], - "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "react-dom/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], @@ -2919,18 +2649,12 @@ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "rhea/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "rhea-promise/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "sass/immutable": ["immutable@4.3.7", "", {}, "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="], "schema-utils/ajv": ["ajv@6.12.6", "", { "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" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "send/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "send/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "send/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -2953,24 +2677,18 @@ "socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "socks-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "solc/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "solc/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "sshpk/jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "superagent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "superagent/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], "supertest/superagent": ["superagent@8.1.2", "", { "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^2.1.2", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0", "semver": "^7.3.8" } }, "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA=="], @@ -2985,8 +2703,6 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "type-graphql/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], @@ -3001,40 +2717,6 @@ "unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "verdaccio/@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw=="], - - "verdaccio/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "verdaccio/@verdaccio/utils": ["@verdaccio/utils@7.0.1-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "lodash": "4.17.21", "minimatch": "7.4.6", "semver": "7.6.3" } }, "sha512-cyJdRrVa+8CS7UuIQb3K3IJFjMe64v38tYiBnohSmhRbX7dX9IT3jWbjrwkqWh4KeS1CS6BYENrGG1evJ2ggrQ=="], - - "verdaccio/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "verdaccio/express": ["express@4.21.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng=="], - - "verdaccio/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "verdaccio/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - - "verdaccio/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - - "verdaccio/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "verdaccio-audit/@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw=="], - - "verdaccio-audit/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "verdaccio-audit/express": ["express@4.21.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng=="], - - "verdaccio-audit/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "verdaccio-htpasswd/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], - - "verdaccio-htpasswd/@verdaccio/file-locking": ["@verdaccio/file-locking@13.0.0-next-8.0", "", { "dependencies": { "lockfile": "1.0.4" } }, "sha512-28XRwpKiE3Z6KsnwE7o8dEM+zGWOT+Vef7RVJyUlG176JVDbGGip3HfCmFioE1a9BklLyGEFTu6D69BzfbRkzA=="], - - "verdaccio-htpasswd/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "verdaccio-htpasswd/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], "vite/rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], @@ -3059,8 +2741,6 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@cypress/request/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], - "@inquirer/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "@inquirer/core/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3105,108 +2785,6 @@ "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@verdaccio/auth/@verdaccio/config/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/auth/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "@verdaccio/auth/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "@verdaccio/auth/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "@verdaccio/auth/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@verdaccio/auth/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/auth/@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/auth/@verdaccio/utils/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/core/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "@verdaccio/logger-commons/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "@verdaccio/logger-commons/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "@verdaccio/logger-commons/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "@verdaccio/logger-commons/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@verdaccio/logger-commons/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/logger/pino/pino-abstract-transport": ["pino-abstract-transport@1.1.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA=="], - - "@verdaccio/logger/pino/pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], - - "@verdaccio/logger/pino/process-warning": ["process-warning@3.0.0", "", {}, "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="], - - "@verdaccio/logger/pino/sonic-boom": ["sonic-boom@3.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA=="], - - "@verdaccio/logger/pino/thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], - - "@verdaccio/middleware/@verdaccio/config/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/middleware/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "@verdaccio/middleware/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "@verdaccio/middleware/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "@verdaccio/middleware/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@verdaccio/middleware/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/middleware/@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/middleware/@verdaccio/utils/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/middleware/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - - "@verdaccio/middleware/express/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - - "@verdaccio/middleware/express/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], - - "@verdaccio/middleware/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@verdaccio/middleware/express/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "@verdaccio/middleware/express/finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], - - "@verdaccio/middleware/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "@verdaccio/middleware/express/path-to-regexp": ["path-to-regexp@0.1.10", "", {}, "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="], - - "@verdaccio/middleware/express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], - - "@verdaccio/middleware/express/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - - "@verdaccio/middleware/express/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - - "@verdaccio/tarball/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "@verdaccio/tarball/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "@verdaccio/tarball/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "@verdaccio/tarball/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@verdaccio/tarball/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/tarball/@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/tarball/@verdaccio/utils/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/url/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "@verdaccio/url/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "@verdaccio/url/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "@verdaccio/url/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "@verdaccio/url/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "@verdaccio/utils/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -3355,14 +2933,6 @@ "msw/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "peek-stream/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "pumpify/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "sass/chokidar/readdirp": ["readdirp@4.0.2", "", {}, "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA=="], "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -3375,108 +2945,16 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "supertest/superagent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "supertest/superagent/formidable": ["formidable@2.1.2", "", { "dependencies": { "dezalgo": "^1.0.4", "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" } }, "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g=="], "table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "through2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "typeorm/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "unstorage/chokidar/readdirp": ["readdirp@4.0.2", "", {}, "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA=="], - "verdaccio-audit/@verdaccio/config/@verdaccio/utils": ["@verdaccio/utils@7.0.1-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "lodash": "4.17.21", "minimatch": "7.4.6", "semver": "7.6.3" } }, "sha512-cyJdRrVa+8CS7UuIQb3K3IJFjMe64v38tYiBnohSmhRbX7dX9IT3jWbjrwkqWh4KeS1CS6BYENrGG1evJ2ggrQ=="], - - "verdaccio-audit/@verdaccio/config/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - - "verdaccio-audit/@verdaccio/config/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "verdaccio-audit/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "verdaccio-audit/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "verdaccio-audit/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "verdaccio-audit/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "verdaccio-audit/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "verdaccio-audit/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - - "verdaccio-audit/express/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - - "verdaccio-audit/express/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], - - "verdaccio-audit/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "verdaccio-audit/express/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "verdaccio-audit/express/finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], - - "verdaccio-audit/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "verdaccio-audit/express/path-to-regexp": ["path-to-regexp@0.1.10", "", {}, "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="], - - "verdaccio-audit/express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], - - "verdaccio-audit/express/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - - "verdaccio-audit/express/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - - "verdaccio-audit/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "verdaccio-audit/https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "verdaccio-htpasswd/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "verdaccio-htpasswd/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "verdaccio-htpasswd/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "verdaccio-htpasswd/@verdaccio/core/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "verdaccio/@verdaccio/config/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "verdaccio/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "verdaccio/@verdaccio/core/core-js": ["core-js@3.37.1", "", {}, "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw=="], - - "verdaccio/@verdaccio/core/http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - - "verdaccio/@verdaccio/core/process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "verdaccio/@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "verdaccio/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - - "verdaccio/express/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - - "verdaccio/express/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], - - "verdaccio/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "verdaccio/express/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "verdaccio/express/finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], - - "verdaccio/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "verdaccio/express/path-to-regexp": ["path-to-regexp@0.1.10", "", {}, "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="], - - "verdaccio/express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], - - "verdaccio/express/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - - "verdaccio/express/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="], "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="], @@ -3571,24 +3049,6 @@ "@testing-library/dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "@verdaccio/auth/@verdaccio/config/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "@verdaccio/auth/@verdaccio/utils/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "@verdaccio/middleware/@verdaccio/config/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "@verdaccio/middleware/@verdaccio/utils/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "@verdaccio/middleware/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@verdaccio/middleware/express/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "@verdaccio/middleware/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@verdaccio/tarball/@verdaccio/utils/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -3607,46 +3067,10 @@ "msw/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "peek-stream/duplexify/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "peek-stream/duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "peek-stream/duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "pumpify/duplexify/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "pumpify/duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "pumpify/duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "typeorm/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "verdaccio-audit/@verdaccio/config/@verdaccio/utils/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - - "verdaccio-audit/@verdaccio/config/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "verdaccio-audit/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "verdaccio-audit/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "verdaccio-audit/express/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "verdaccio-audit/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "verdaccio/@verdaccio/config/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "verdaccio/@verdaccio/utils/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "verdaccio/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "verdaccio/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "verdaccio/express/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "verdaccio/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "args/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], diff --git a/test/bundler/bundler_bytecode_portable.test.ts b/test/bundler/bundler_bytecode_portable.test.ts index b530a7807eb7..5b7063513355 100644 --- a/test/bundler/bundler_bytecode_portable.test.ts +++ b/test/bundler/bundler_bytecode_portable.test.ts @@ -511,10 +511,10 @@ describe("bytecode cache portability", () => { }, }, "bun build --bytecode libraries.js": { - "js": "493bab674ff49b287f26be3f356a3ad6681afb0c7eeffaa590f10cdcd8b58724", + "js": "cb63dc7b9a0d57183c02aac96e13182312fdefd64d89da2ad4a60a35d63ff2af", "jsc": { - "bytes": 21206912, - "sha256": "49e423d3252b0e1ed7e9529d6475026ccb9fb8af73e746cad15d8420e07fae54", + "bytes": 21174272, + "sha256": "7ae6cdd7092d140bfbad79e7f304b14e24f8ece485df9f779092ffde1351e21f", }, }, "bun build --bytecode lodash/lodash.js": { diff --git a/test/bunfig.toml b/test/bunfig.toml index 9ee12ca1fa57..7c85615a13af 100644 --- a/test/bunfig.toml +++ b/test/bunfig.toml @@ -3,7 +3,5 @@ preload = "./preload.ts" [install] linker = "isolated" -# See ../bunfig.toml — CI deletes the install cache between steps, and -# verdaccio's internal packages have phantom dependencies that the global -# store's stricter isolation catches. +# See ../bunfig.toml: CI deletes the install cache between steps. globalStore = false diff --git a/test/cli/install/bun-add-catalog.test.ts b/test/cli/install/bun-add-catalog.test.ts index fdd2154c7ee0..96f52e381493 100644 --- a/test/cli/install/bun-add-catalog.test.ts +++ b/test/cli/install/bun-add-catalog.test.ts @@ -2,10 +2,11 @@ import { file, write } from "bun"; import { readTarball } from "bun:internal-for-testing"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync } from "fs"; -import { VerdaccioRegistry, bunEnv, bunExe, readdirSorted, runBunInstall } from "harness"; +import { bunEnv, bunExe, readdirSorted, runBunInstall } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-add-filter.test.ts b/test/cli/install/bun-add-filter.test.ts index 42b3f00aa574..626c31d265bd 100644 --- a/test/cli/install/bun-add-filter.test.ts +++ b/test/cli/install/bun-add-filter.test.ts @@ -1,10 +1,11 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { chmod, exists, mkdir, rm } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; +import { bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-audit.test.ts b/test/cli/install/bun-audit.test.ts index b0197c013fb3..acaf59331168 100644 --- a/test/cli/install/bun-audit.test.ts +++ b/test/cli/install/bun-audit.test.ts @@ -3,7 +3,6 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { exists, readlink } from "fs/promises"; import { DirectoryTree, - VerdaccioRegistry, bunEnv, bunExe, gunzipJsonRequest, @@ -12,6 +11,7 @@ import { tempDir, } from "harness"; import { join } from "node:path"; +import { TestRegistry } from "registry"; import { resolveBulkAdvisoryFixture } from "./registry/fixtures/audit/audit-fixtures"; function fixture( @@ -25,7 +25,7 @@ function fixture( } let server: Bun.Server; -const verdaccio = new VerdaccioRegistry(); +const verdaccio = new TestRegistry(); beforeAll(async () => { server = Bun.serve({ @@ -125,7 +125,7 @@ type RegistryOptions = { rewriteTime?: Record>; }; -// Answers the bulk-advisory endpoint itself and proxies everything else to verdaccio, pointing manifest tarball URLs back at itself. +// Answers the bulk-advisory endpoint itself and proxies everything else to the fixture registry, pointing manifest tarball URLs back at itself. function startRegistry(advisories: Record, options: RegistryOptions = {}) { let bulkRequests = 0; return Bun.serve({ diff --git a/test/cli/install/bun-dedupe.test.ts b/test/cli/install/bun-dedupe.test.ts index 5acae0cba2e9..ba3adc4ad4c4 100644 --- a/test/cli/install/bun-dedupe.test.ts +++ b/test/cli/install/bun-dedupe.test.ts @@ -1,10 +1,11 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { copyFile, exists, mkdir, realpath, rm } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, normalizeBunSnapshot, readdirSorted, runBunInstall } from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot, readdirSorted, runBunInstall } from "harness"; import { dirname, join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts index 5c40d26fd7ed..8b2eda5daffa 100644 --- a/test/cli/install/bun-install-lifecycle-scripts.test.ts +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -2,7 +2,6 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; import { exists, mkdir, rm, writeFile } from "fs/promises"; import { - VerdaccioRegistry, assertManifestsPopulated, bunEnv as baseEnv, bunExe, @@ -13,8 +12,9 @@ import { } from "harness"; import { constants as osConstants } from "os"; import { join, sep } from "path"; +import { TestRegistry } from "registry"; -var verdaccio = new VerdaccioRegistry(); +var verdaccio = new TestRegistry(); setDefaultTimeout(1000 * 60 * 5); diff --git a/test/cli/install/bun-install-native-binlink.test.ts b/test/cli/install/bun-install-native-binlink.test.ts index 71df3ee19c83..13c85083921f 100644 --- a/test/cli/install/bun-install-native-binlink.test.ts +++ b/test/cli/install/bun-install-native-binlink.test.ts @@ -2,15 +2,16 @@ import { spawn } from "bun"; import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; import { chmodSync, existsSync, readFileSync, realpathSync, statSync, symlinkSync } from "fs"; import { rm, writeFile } from "fs/promises"; -import { bunEnv, bunExe, isWindows, tempDir, VerdaccioRegistry } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { join, sep } from "path"; +import { TestRegistry } from "registry"; -let verdaccio: VerdaccioRegistry; +let verdaccio: TestRegistry; setDefaultTimeout(1000 * 60 * 5); beforeAll(async () => { - verdaccio = new VerdaccioRegistry(); + verdaccio = new TestRegistry(); await verdaccio.start(); }); diff --git a/test/cli/install/bun-install-patch.test.ts b/test/cli/install/bun-install-patch.test.ts index b4e37a94c824..36e8aa60e90c 100644 --- a/test/cli/install/bun-install-patch.test.ts +++ b/test/cli/install/bun-install-patch.test.ts @@ -1,15 +1,9 @@ import { $ } from "bun"; import { afterAll, beforeAll, describe, expect, it, setDefaultTimeout, test } from "bun:test"; import { rmSync } from "fs"; -import { - bunEnv, - bunExe, - normalizeBunSnapshot as normalizeBunSnapshot_, - runBunInstall, - tempDir, - VerdaccioRegistry, -} from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot as normalizeBunSnapshot_, runBunInstall, tempDir } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; import { pathToFileURL } from "url"; const normalizeBunSnapshot = (str: string) => { @@ -1136,7 +1130,7 @@ describe("patchedDependencies contents_hash", () => { // install failed with "Couldn't find patch file" because the dependency's patch // path was resolved against the consumer's root (#13531). describe("patchedDependencies declared by a dependency", () => { - const registry = new VerdaccioRegistry(); + const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 91bb302c2df2..59a7a4d33bb5 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -23,11 +23,11 @@ import { toBeValidBin, toHaveBins, toMatchNodeModulesAt, - VerdaccioRegistry, writeShebangScript, } from "harness"; import { createServer as createTcpServer, connect as tcpConnect, type Socket } from "net"; import { join, resolve } from "path"; +import { TestRegistry } from "registry"; import { createServer as createTlsServer } from "tls"; const { parseLockfile } = install_test_helpers; @@ -37,21 +37,17 @@ expect.extend({ toMatchNodeModulesAt, }); -var registry: VerdaccioRegistry; +var registry: TestRegistry; var port: number; var packageDir: string; /** packageJson = join(packageDir, "package.json"); */ var packageJson: string; -let users: Record = {}; - setDefaultTimeout(1000 * 60 * 5); -registry = new VerdaccioRegistry(); +registry = new TestRegistry().start(); port = registry.port; -await registry.start(); -afterAll(async () => { - await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); +afterAll(() => { registry.stop(); }); @@ -59,9 +55,6 @@ beforeEach(async () => { ({ packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false, linker: "hoisted" }, })); - await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); - users = {}; env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); }); @@ -70,37 +63,6 @@ function registryUrl() { return registry.registryUrl(); } -/** - * Returns auth token - */ -async function generateRegistryUser(username: string, password: string): Promise { - if (users[username]) { - throw new Error("that user already exists"); - } else users[username] = password; - - const url = `http://localhost:${port}/-/user/org.couchdb.user:${username}`; - const user = { - name: username, - password: password, - email: `${username}@example.com`, - }; - - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(user), - }); - - if (response.ok) { - const data = await response.json(); - return data.token; - } else { - throw new Error("Failed to create user:", response.statusText); - } -} - describe("auto-install", () => { test("symlinks (and junctions) are created correctly in the install cache", async () => { const { stdout, stderr, exited } = spawn({ @@ -605,7 +567,7 @@ describe("whoami", async () => { expect(await exited).toBe(0); }); test("only .npmrc", async () => { - const token = await generateRegistryUser("whoami-npmrc", "whoami-npmrc"); + const token = await registry.generateUser("whoami-npmrc", "whoami-npmrc"); const npmrc = ` //localhost:${port}/:_authToken=${token} registry=http://localhost:${port}`; @@ -627,7 +589,7 @@ describe("whoami", async () => { expect(await exited).toBe(0); }); test("two .npmrc", async () => { - const token = await generateRegistryUser("whoami-two-npmrc", "whoami-two-npmrc"); + const token = await registry.generateUser("whoami-two-npmrc", "whoami-two-npmrc"); const packageNpmrc = `registry=http://localhost:${port}/`; const homeNpmrc = `//localhost:${port}/:_authToken=${token}`; const homeDir = `${packageDir}/home_dir`; @@ -670,7 +632,7 @@ describe("whoami", async () => { }); test("invalid token", async () => { // create the user and provide an invalid token - const token = await generateRegistryUser("invalid-token", "invalid-token"); + await registry.generateUser("invalid-token", "invalid-token"); const bunfig = Bun.TOML.stringify({ install: { cache: false, @@ -692,7 +654,33 @@ describe("whoami", async () => { const out = await stdout.text(); expect(out).toBeEmpty(); const err = await stderr.text(); - expect(err).toBe(`error: failed to authenticate with registry 'http://localhost:${port}/'\n`); + expect(err).toBe(`\n401 Unauthorized: http://localhost:${port}/-/whoami\n`); + expect(await exited).toBe(1); + }); + test("a registry that answers 200 without a username", async () => { + // Some registries do not reject a token they do not know. They answer with an empty object. + using anonymous = Bun.serve({ port: 0, fetch: () => Response.json({}) }); + const bunfig = Bun.TOML.stringify({ + install: { + cache: false, + registry: { url: `http://localhost:${anonymous.port}/`, token: "1234567" }, + }, + }); + await Promise.all([ + write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), + write(join(packageDir, "bunfig.toml"), bunfig), + ]); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "whoami"], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + const out = await stdout.text(); + expect(out).toBeEmpty(); + const err = await stderr.text(); + expect(err).toBe(`error: failed to authenticate with registry 'http://localhost:${anonymous.port}/'\n`); expect(await exited).toBe(1); }); }); @@ -10048,7 +10036,7 @@ test("rejects npm aliases whose manifest URL resolves to a different host than t }, }); - const token = await generateRegistryUser("manifest-host-pinning", "manifest-host-pinning"); + const token = await registry.generateUser("manifest-host-pinning", "manifest-host-pinning"); await Promise.all([ write( join(packageDir, "bunfig.toml"), diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index c8ba27d17e5f..82c63e1f3102 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -11,15 +11,15 @@ import { runBunInstall, tempDir, toBeValidBin, - VerdaccioRegistry, } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; expect.extend({ toBeValidBin, }); -var registry = new VerdaccioRegistry(); +var registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index d2fcd8200458..6dc43eefec1e 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -1,10 +1,11 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, expect, it } from "bun:test"; import { copyFile, exists, open, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, isWindows, runBunInstall, VerdaccioRegistry } from "harness"; +import { bunExe, bunEnv as env, isWindows, runBunInstall } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-patch.test.ts b/test/cli/install/bun-patch.test.ts index 0e61d4526398..bd8784a84d7d 100644 --- a/test/cli/install/bun-patch.test.ts +++ b/test/cli/install/bun-patch.test.ts @@ -1,8 +1,9 @@ import { $, ShellOutput } from "bun"; import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; import { lstatSync, readFileSync } from "fs"; -import { bunEnv, bunExe, isASAN, tempDir, VerdaccioRegistry } from "harness"; +import { bunEnv, bunExe, isASAN, tempDir } from "harness"; import { isAbsolute, join, sep } from "path"; +import { TestRegistry } from "registry"; const expectNoError = (o: ShellOutput) => expect(o.stderr.toString()).not.toContain("error"); // const platformPath = (path: string) => (process.platform === "win32" ? path.replaceAll("/", sep) : path); @@ -71,7 +72,7 @@ describe("error messages", () => { // stack buffers (512 bytes in the installer itself), so a long enough spec crashed // every command that formatted it. describe("packages whose label is longer than 1024 bytes", () => { - const registry = new VerdaccioRegistry(); + const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-pm-licenses.test.ts b/test/cli/install/bun-pm-licenses.test.ts index 0c70bd89b73c..0cdc74db6ce3 100644 --- a/test/cli/install/bun-pm-licenses.test.ts +++ b/test/cli/install/bun-pm-licenses.test.ts @@ -1,8 +1,9 @@ import { spawn } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs"; -import { VerdaccioRegistry, bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness"; import { isAbsolute, join, sep } from "path"; +import { TestRegistry } from "registry"; import { pathToFileURL } from "url"; type Linker = "hoisted" | "isolated"; @@ -17,7 +18,7 @@ type LicenseEntry = { description?: string; }; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/bun-prune.test.ts b/test/cli/install/bun-prune.test.ts index b9162889a85c..72a2c7c0a84d 100644 --- a/test/cli/install/bun-prune.test.ts +++ b/test/cli/install/bun-prune.test.ts @@ -1,6 +1,6 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, normalizeBunSnapshot, runBunInstall, tempDir, VerdaccioRegistry } from "harness"; +import { bunEnv, bunExe, isWindows, normalizeBunSnapshot, runBunInstall, tempDir } from "harness"; import { chmodSync, closeSync, @@ -18,8 +18,9 @@ import { } from "node:fs"; import { basename, join } from "node:path"; import { pathToFileURL } from "node:url"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); @@ -125,7 +126,7 @@ function expectBinRemoved(nm: string, name: string) { } } -type BunfigOpts = NonNullable[0]>["bunfigOpts"]; +type BunfigOpts = NonNullable[0]>["bunfigOpts"]; async function setup(pkgJson: Record, bunfigOpts?: BunfigOpts) { const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts }); diff --git a/test/cli/install/bun-publish.test.ts b/test/cli/install/bun-publish.test.ts index c701b322806c..c7bee73073f0 100644 --- a/test/cli/install/bun-publish.test.ts +++ b/test/cli/install/bun-publish.test.ts @@ -2,20 +2,11 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { exists, rm } from "fs/promises"; -import { - VerdaccioRegistry, - bunExe, - bunEnv as env, - isLinux, - isWindows, - pack, - runBunInstall, - tempDir, - tmpdirSync, -} from "harness"; +import { bunExe, bunEnv as env, isLinux, isWindows, pack, runBunInstall, tempDir, tmpdirSync } from "harness"; import { delimiter, join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); @@ -121,7 +112,7 @@ describe("otp", async () => { }, }); await Promise.all([ - rm(join(registry.packagesPath, "otp-pkg-1"), { recursive: true, force: true }), + registry.packages.delete("otp-pkg-1"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -156,7 +147,7 @@ describe("otp", async () => { }); await Promise.all([ - rm(join(registry.packagesPath, "otp-pkg-2"), { recursive: true, force: true }), + registry.packages.delete("otp-pkg-2"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -211,7 +202,7 @@ describe("otp", async () => { }); await Promise.all([ - rm(join(registry.packagesPath, "otp-pkg-5"), { recursive: true, force: true }), + registry.packages.delete("otp-pkg-5"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -415,7 +406,7 @@ describe("otp", async () => { }); await Promise.all([ - rm(join(registry.packagesPath, "otp-pkg-3"), { recursive: true, force: true }), + registry.packages.delete("otp-pkg-3"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -461,7 +452,7 @@ describe("otp", async () => { }); await Promise.all([ - rm(join(registry.packagesPath, "otp-pkg-4"), { recursive: true, force: true }), + registry.packages.delete("otp-pkg-4"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -488,7 +479,7 @@ test("can publish a package then install it", async () => { const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("basic"); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-1"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-1"), write( packageJson, JSON.stringify({ @@ -510,6 +501,31 @@ test("can publish a package then install it", async () => { await runBunInstall(env, packageDir); expect(await exists(join(packageDir, "node_modules", "publish-pkg-1", "package.json"))).toBeTrue(); }); +test("the publish request has the Content-Type that verdaccio accepts", async () => { + // verdaccio compares the header with `application/json`, character for character, and answers 415 to + // `application/json; charset=utf-8`. The registry of these tests does the same, so every publish in this file + // fails if bun changes the header. This test says which header it is. + using recording = new TestRegistry({ recordRequests: true }).start(); + const { packageDir, packageJson } = await recording.createTestDir(); + await Promise.all([ + write(packageJson, JSON.stringify({ name: "publish-content-type", version: "1.0.0" })), + write(join(packageDir, "bunfig.toml"), await recording.authBunfig("content-type")), + ]); + recording.requests.length = 0; + + const { err, exitCode } = await publish(env, packageDir); + expect(err).not.toContain("error:"); + expect( + recording.requests.map(({ method, path, status, headers }) => ({ + method, + path, + status, + "content-type": headers["content-type"], + })), + ).toEqual([{ method: "PUT", path: "/publish-content-type", status: 200, "content-type": "application/json" }]); + expect(exitCode).toBe(0); +}); + test("can publish from a tarball", async () => { const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("tarball"); @@ -521,7 +537,7 @@ test("can publish from a tarball", async () => { }, }; await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-2"), write(packageJson, JSON.stringify(json)), write(join(packageDir, "bunfig.toml"), bunfig), ]); @@ -537,7 +553,7 @@ test("can publish from a tarball", async () => { expect(await exists(join(packageDir, "node_modules", "publish-pkg-2", "package.json"))).toBeTrue(); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-2"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-2"), rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }), rm(join(packageDir, "node_modules"), { recursive: true, force: true }), ]); @@ -562,7 +578,7 @@ test("can publish scoped packages", async () => { }, }; await Promise.all([ - rm(join(registry.packagesPath, "@scoped", "pkg-1"), { recursive: true, force: true }), + registry.packages.delete("@scoped/pkg-1"), write(packageJson, JSON.stringify(json)), write(join(packageDir, "bunfig.toml"), bunfig), ]); @@ -587,7 +603,7 @@ for (const info of [ const bunfig = await registry.authBunfig("binaries-" + info.user); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-" + info.user), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-" + info.user), write( join(publishDir, "package.json"), JSON.stringify({ @@ -659,7 +675,7 @@ test("dependencies are installed", async () => { const publishDir = tmpdirSync(); const bunfig = await registry.authBunfig("manydeps"); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-deps"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-deps"), write( join(publishDir, "package.json"), JSON.stringify( @@ -720,7 +736,7 @@ test("can publish workspace package", async () => { }, }; await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-3"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-3"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -746,7 +762,7 @@ describe("--dry-run", async () => { const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("dryrun"); await Promise.all([ - rm(join(registry.packagesPath, "dry-run-1"), { recursive: true, force: true }), + registry.packages.delete("dry-run-1"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -763,13 +779,13 @@ describe("--dry-run", async () => { const { out, err, exitCode } = await publish(env, packageDir, "--dry-run"); expect(exitCode).toBe(0); - expect(await exists(join(registry.packagesPath, "dry-run-1"))).toBeFalse(); + expect(await registry.packages.has("dry-run-1")).toBeFalse(); }); test("does not publish from tarball path", async () => { const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("dryruntarball"); await Promise.all([ - rm(join(registry.packagesPath, "dry-run-2"), { recursive: true, force: true }), + registry.packages.delete("dry-run-2"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -788,7 +804,7 @@ describe("--dry-run", async () => { const { out, err, exitCode } = await publish(env, packageDir, "./dry-run-2-2.2.2.tgz", "--dry-run"); expect(exitCode).toBe(0); - expect(await exists(join(registry.packagesPath, "dry-run-2"))).toBeFalse(); + expect(await registry.packages.has("dry-run-2")).toBeFalse(); }); test("registry summary line does not print userinfo from the registry url", async () => { const packageDir = tmpdirSync(); @@ -954,7 +970,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("lifecycle" + (arg.length > 0 ? "dry" : "")); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-4"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-4"), write(packageJson, JSON.stringify(json)), write(join(packageDir, "script.js"), script), write(join(packageDir, "bunfig.toml"), bunfig), @@ -987,7 +1003,7 @@ postpack: \${fs.existsSync("postpack.txt")}\`)`; const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("ignorescripts"); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-5"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-4"), write(packageJson, JSON.stringify(json)), write(join(packageDir, "script.js"), script), write(join(packageDir, "bunfig.toml"), bunfig), @@ -1018,7 +1034,7 @@ test("prepublishOnly modifying version publishes correct version (#17195)", asyn fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2));`; await Promise.all([ - rm(join(registry.packagesPath, "publish-version-update"), { recursive: true, force: true }), + registry.packages.delete("publish-version-update"), write( packageJson, JSON.stringify({ @@ -1050,7 +1066,7 @@ test("attempting to publish a private package should fail", async () => { const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("privatepackage"); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-6"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-6"), write( packageJson, JSON.stringify({ @@ -1069,7 +1085,7 @@ test("attempting to publish a private package should fail", async () => { let { out, err, exitCode } = await publish(env, packageDir); expect(exitCode).toBe(1); expect(err).toContain("error: attempted to publish a private package"); - expect(await exists(join(registry.packagesPath, "publish-pkg-6-6.6.6.tgz"))).toBeFalse(); + expect(await registry.packages.has("publish-pkg-6")).toBeFalse(); // try tarball await pack(packageDir, env); @@ -1084,7 +1100,7 @@ describe("access", async () => { const { packageDir, packageJson } = await registry.createTestDir(); const bunfig = await registry.authBunfig("accessflag"); await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-7"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-7"), write(join(packageDir, "bunfig.toml"), bunfig), write( packageJson, @@ -1103,7 +1119,7 @@ describe("access", async () => { ({ out, err, exitCode } = await publish(env, packageDir, "--access", "public")); expect(exitCode).toBe(0); - expect(await exists(join(registry.packagesPath, "publish-pkg-7"))).toBeTrue(); + expect(await registry.packages.has("publish-pkg-7")).toBeTrue(); }); for (const access of ["restricted", "public"]) { @@ -1123,7 +1139,7 @@ describe("access", async () => { }; await Promise.all([ - rm(join(registry.packagesPath, "@secret", "publish-pkg-8"), { recursive: true, force: true }), + registry.packages.delete("@secret/publish-pkg-8"), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1152,7 +1168,7 @@ describe("tag", async () => { }, }; await Promise.all([ - rm(join(registry.packagesPath, "publish-pkg-9"), { recursive: true, force: true }), + registry.packages.delete("publish-pkg-9"), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1178,7 +1194,7 @@ it("$npm_command is accurate during publish", async () => { }), ); await write(join(packageDir, "bunfig.toml"), await registry.authBunfig("npm_command")); - await rm(join(registry.packagesPath, "publish-pkg-10"), { recursive: true, force: true }); + registry.packages.delete("publish-pkg-10"); let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); expect(err).toBe(`$ echo $npm_command\n`); expect(out.split("\n")).toEqual([ @@ -1218,7 +1234,7 @@ it("$npm_lifecycle_event is accurate during publish", async () => { `, ); await write(join(packageDir, "bunfig.toml"), await registry.authBunfig("npm_lifecycle_event")); - await rm(join(registry.packagesPath, "publish-pkg-11"), { recursive: true, force: true }); + registry.packages.delete("publish-pkg-11"); let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`); expect(out.split("\n")).toEqual([ @@ -1362,7 +1378,7 @@ describe("--tolerate-republish", async () => { }; await Promise.all([ - rm(join(registry.packagesPath, "republish-test-1"), { recursive: true, force: true }), + registry.packages.delete("republish-test-1"), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1375,7 +1391,8 @@ describe("--tolerate-republish", async () => { // Second publish should fail ({ out, err, exitCode } = await publish(env, packageDir)); expect(exitCode).toBe(1); - expect(err).toMatch(/403|409|already exists|already present|cannot publish/); + expect(err).toContain("403 Forbidden"); + expect(err).toContain("You cannot publish over the previously published versions: 1.0.0."); }); test("republishing with --tolerate-republish skips when version exists", async () => { @@ -1387,7 +1404,7 @@ describe("--tolerate-republish", async () => { }; await Promise.all([ - rm(join(registry.packagesPath, "republish-test-2"), { recursive: true, force: true }), + registry.packages.delete("republish-test-2"), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); @@ -1413,7 +1430,7 @@ describe("--tolerate-republish", async () => { }; await Promise.all([ - rm(join(registry.packagesPath, "republish-test-3"), { recursive: true, force: true }), + registry.packages.delete("republish-test-3"), write(join(packageDir, "bunfig.toml"), bunfig), write(packageJson, JSON.stringify(pkgJson)), ]); diff --git a/test/cli/install/bun-update-lockfile-sync.test.ts b/test/cli/install/bun-update-lockfile-sync.test.ts index d83f6eb72cf7..c0c8c8f435d0 100644 --- a/test/cli/install/bun-update-lockfile-sync.test.ts +++ b/test/cli/install/bun-update-lockfile-sync.test.ts @@ -1,12 +1,13 @@ import { Archive, file, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { appendFile, exists } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, normalizeBunSnapshot, runBunInstall } from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot, runBunInstall } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; // Registry: no-deps 1.0.0/1.0.1/1.1.0/2.0.0, @types/no-deps 1.0.0/2.0.0, a-dep 1.0.1..1.0.10, one-range-dep@1.0.0 -> no-deps ^1.0.0, dep-with-tags 1.0.0..3.0.1 (latest=3.0.0, pre-2=2.0.1). -const verdaccio = new VerdaccioRegistry(); +const verdaccio = new TestRegistry(); beforeAll(async () => { await verdaccio.start(); diff --git a/test/cli/install/bun-update-transitive.test.ts b/test/cli/install/bun-update-transitive.test.ts index 7607ca55ef05..2e8ed9495b23 100644 --- a/test/cli/install/bun-update-transitive.test.ts +++ b/test/cli/install/bun-update-transitive.test.ts @@ -1,12 +1,13 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { exists, rm } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; // Registry: no-deps 1.0.0/1.0.1/1.1.0/2.0.0, a-dep 1.0.1..1.0.10, @types/no-deps 1.0.0/2.0.0, one-range-dep@1.0.0 -> no-deps ^1.0.0, one-fixed-dep@1.0.0 -> no-deps 1.0.0, dep-with-tags latest=3.0.0, pre-2=2.0.1, 3.0.1 published above latest. -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); @@ -1234,7 +1235,7 @@ test.concurrent("in a workspace, `bun update` from one member also re-points a s type Manifests = Record }>>; type Tags = Record>; -// Serves one manifest per name from memory; verdaccio has no parent whose newer version keeps a range on the same child, and its dist-tags cannot move mid-test. `tags` is read per request, so a test can move a tag after installing. +// Serves one manifest per name from memory; the fixture registry has no parent whose newer version keeps a range on the same child, and no test moves its dist-tags. `tags` is read per request, so a test can move a tag after installing. // `status` is keyed by package name or by tarball file name ("leaf-1.1.0.tgz"); `tarballOrigin` replaces this server's origin in every `dist.tarball`. type RegistryKnobs = { times?: Record>; @@ -1569,7 +1570,7 @@ test.concurrent( }, ); -// Mirrors verdaccio's dep-with-tags: 3.0.1 is published above `latest` (3.0.0), which `bun install` prefers whenever the range allows it. +// Mirrors the fixture package dep-with-tags: 3.0.1 is published above `latest` (3.0.0), which `bun install` prefers whenever the range allows it. const ABOVE_LATEST: Manifests = { parent: { "1.0.0": { dependencies: { leaf: ">=1.0.0" } } }, leaf: { "1.0.0": {}, "1.0.1": {}, "2.0.0": {}, "2.0.1": {}, "3.0.0": {}, "3.0.1": {} }, diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index a070c02ba70a..33bd279c0121 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -1,8 +1,9 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; import { access, appendFile, exists, mkdir, readFile, rm, writeFile } from "fs/promises"; -import { VerdaccioRegistry, bunExe, bunEnv as env, pack, readdirSorted, toBeValidBin, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, pack, readdirSorted, toBeValidBin, toHaveBins } from "harness"; import { basename, dirname, join } from "path"; +import { TestRegistry } from "registry"; import { dummyAfterAll, dummyAfterEach, @@ -1692,7 +1693,7 @@ it("bun update rejects a name that is not in the lockfile", async () => { // Registry: no-deps 1.0.0/1.0.1/1.1.0/2.0.0; a-dep 1.0.1..1.0.10; dep-with-tags latest=3.0.0, pre-2=2.0.1; @types/* 1.0.0/2.0.0. describe("bun update semantics", () => { type Json = Record; - const verdaccio = new VerdaccioRegistry(); + const verdaccio = new TestRegistry(); beforeAll(async () => { await verdaccio.start(); diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index cfde26eca4d9..7a7604ff674d 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -12,20 +12,20 @@ import { runBunInstall, runBunUpdate, toMatchNodeModulesAt, - VerdaccioRegistry, } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; const { parseLockfile } = install_test_helpers; expect.extend({ toMatchNodeModulesAt }); -var verdaccio: VerdaccioRegistry; +var verdaccio: TestRegistry; setDefaultTimeout(1000 * 60 * 5); beforeAll(async () => { - verdaccio = new VerdaccioRegistry(); + verdaccio = new TestRegistry(); await verdaccio.start(); }); diff --git a/test/cli/install/catalogs.test.ts b/test/cli/install/catalogs.test.ts index 159c6aabe4aa..cf050a97b863 100644 --- a/test/cli/install/catalogs.test.ts +++ b/test/cli/install/catalogs.test.ts @@ -3,14 +3,15 @@ import { readTarball } from "bun:internal-for-testing"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync, lstatSync, readlinkSync } from "fs"; import { exists, readdir, realpath, rm } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, normalizeBunSnapshot, pack, runBunInstall, tempDir } from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot, pack, runBunInstall, tempDir } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -var registry = new VerdaccioRegistry(); +var registry = new TestRegistry(); type Manifests = Record>>>; -// Registry packages that ship a raw `catalog:` specifier; verdaccio has none. +// Registry packages that ship a raw `catalog:` specifier; the fixture registry has none. const catalogManifests: Manifests = { "leaf": { "1.0.0": {}, "2.0.0": {} }, "wants-leaf-peer": { "1.0.0": { peerDependencies: { leaf: "catalog:" } } }, diff --git a/test/cli/install/config-precedence.test.ts b/test/cli/install/config-precedence.test.ts index 1dc220cff80c..d21a9e90e401 100644 --- a/test/cli/install/config-precedence.test.ts +++ b/test/cli/install/config-precedence.test.ts @@ -1,21 +1,10 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync, readFileSync, readdirSync } from "fs"; -import { VerdaccioRegistry, bunEnv, bunExe, readdirSorted, tempDir } from "harness"; +import { bunEnv, bunExe, readdirSorted, tempDir } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -// Own config dir: other install files' VerdaccioRegistry start()/stop() delete the shared htpasswd, invalidating our token. -const sharedRegistryDir = join(import.meta.dir, "registry"); -const sharedVerdaccioConfig = readFileSync(join(sharedRegistryDir, "verdaccio.yaml"), "utf8"); -if (!sharedVerdaccioConfig.includes("storage: ./packages")) { - throw new Error("registry/verdaccio.yaml no longer has a 'storage: ./packages' line to redirect"); -} -const registryDir = tempDir("config-precedence-registry", { - "verdaccio.yaml": sharedVerdaccioConfig.replace( - "storage: ./packages", - `storage: ${JSON.stringify(join(sharedRegistryDir, "packages"))}`, - ), -}); -const registry = new VerdaccioRegistry({ configPath: join(String(registryDir), "verdaccio.yaml") }); +const registry = new TestRegistry(); let authToken: string; beforeAll(async () => { @@ -25,7 +14,6 @@ beforeAll(async () => { afterAll(() => { registry.stop(); - registryDir[Symbol.dispose](); }); const authLine = () => `//localhost:${registry.port}/:_authToken=${authToken}\n`; @@ -56,7 +44,7 @@ function deadRegistry() { }; } -/** Forwards to verdaccio while recording every Authorization header it receives. */ +/** Forwards to the registry while recording every Authorization header it receives. */ function capturingRegistry() { const authorizations: (string | null)[] = []; const upstream = registry.registryUrl(); diff --git a/test/cli/install/frozen-lockfile-missing-workspace.test.ts b/test/cli/install/frozen-lockfile-missing-workspace.test.ts index 30ee8404fdf7..eaae48835679 100644 --- a/test/cli/install/frozen-lockfile-missing-workspace.test.ts +++ b/test/cli/install/frozen-lockfile-missing-workspace.test.ts @@ -1,10 +1,11 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { exists, rm } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/frozen-lockfile-pruned.test.ts b/test/cli/install/frozen-lockfile-pruned.test.ts index 1f9185079daa..bb88e86a06fc 100644 --- a/test/cli/install/frozen-lockfile-pruned.test.ts +++ b/test/cli/install/frozen-lockfile-pruned.test.ts @@ -1,14 +1,15 @@ import { file, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { exists, lstat } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; +import { bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; import { dirname, join } from "path"; +import { TestRegistry } from "registry"; type Linker = "hoisted" | "isolated"; type PackageJson = Record; type Tree = { root: PackageJson; packages: Record; files?: Record }; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/hoist.test.ts b/test/cli/install/hoist.test.ts index 12919c36b139..9a0a39403ce7 100644 --- a/test/cli/install/hoist.test.ts +++ b/test/cli/install/hoist.test.ts @@ -1,7 +1,8 @@ import { afterAll, beforeAll, test } from "bun:test"; -import { VerdaccioRegistry, bunEnv, runBunInstall } from "harness"; +import { bunEnv, runBunInstall } from "harness"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 4da9a83c8270..ea9aa9c59ab2 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -2,12 +2,13 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync, lstatSync, readFileSync, readlinkSync, statSync } from "fs"; import { mkdir, readlink, rm, symlink } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, readdirSorted, runBunInstall, tempDir } from "harness"; +import { bunEnv, bunExe, readdirSorted, runBunInstall, tempDir } from "harness"; import { createRequire } from "module"; import { basename, dirname, join } from "path"; +import { TestRegistry } from "registry"; import { pathToFileURL } from "url"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); // With the global virtual store enabled, dependency symlinks inside a store // entry point at sibling global-store directories whose names carry a 16-hex @@ -2110,7 +2111,7 @@ test("runs lifecycle scripts correctly", async () => { }); // Self-contained HTTP server that serves package manifests & tarballs -// directly from the Verdaccio fixtures, with Cache-Control: max-age=300 +// directly from the registry fixtures, with Cache-Control: max-age=300 // to replicate npmjs.org behavior (fully synchronous on warm cache). function serveFixtures() { const packagesDir = join(import.meta.dir, "registry", "packages"); diff --git a/test/cli/install/isolated-relink.test.ts b/test/cli/install/isolated-relink.test.ts index a20ca431eb28..3433e0edff77 100644 --- a/test/cli/install/isolated-relink.test.ts +++ b/test/cli/install/isolated-relink.test.ts @@ -2,10 +2,11 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { existsSync, readFileSync } from "fs"; import { lstat, mkdir, realpath, unlink } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; +import { bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; import { dirname, join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/migration/migrate.test.ts b/test/cli/install/migration/migrate.test.ts index 7cd4c5009a56..053665c1f65d 100644 --- a/test/cli/install/migration/migrate.test.ts +++ b/test/cli/install/migration/migrate.test.ts @@ -743,7 +743,7 @@ describe("package-lock.json migration fixes", () => { return dir; } - // Serves the verdaccio fixture packages from disk and records every path requested. + // Serves the registry fixture packages from disk and records every path requested. function localRegistry() { const requests: string[] = []; let url = ""; diff --git a/test/cli/install/migration/pnpm-lock-v9.test.ts b/test/cli/install/migration/pnpm-lock-v9.test.ts index bfc0afca6f13..0f5901d157af 100644 --- a/test/cli/install/migration/pnpm-lock-v9.test.ts +++ b/test/cli/install/migration/pnpm-lock-v9.test.ts @@ -1,9 +1,10 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync, readdirSync, realpathSync, rmSync } from "fs"; -import { bunEnv, bunExe, nodeModulesPackages, tempDir, VerdaccioRegistry } from "harness"; +import { bunEnv, bunExe, nodeModulesPackages, tempDir } from "harness"; import { dirname, join } from "path"; +import { TestRegistry } from "registry"; -const verdaccio = new VerdaccioRegistry(); +const verdaccio = new TestRegistry(); beforeAll(async () => { await verdaccio.start(); diff --git a/test/cli/install/migration/pnpm-migration.test.ts b/test/cli/install/migration/pnpm-migration.test.ts index 40e8b5e4ff8d..4c54b23bb310 100644 --- a/test/cli/install/migration/pnpm-migration.test.ts +++ b/test/cli/install/migration/pnpm-migration.test.ts @@ -1,9 +1,10 @@ import { file, spawn } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunExe, bunEnv as env, nodeModulesPackages, tempDir, VerdaccioRegistry } from "harness.js"; +import { bunExe, bunEnv as env, nodeModulesPackages, tempDir } from "harness.js"; import { join } from "path"; +import { TestRegistry } from "registry"; -let verdaccio = new VerdaccioRegistry(); +let verdaccio = new TestRegistry(); beforeAll(async () => { await verdaccio.start(); diff --git a/test/cli/install/nested-overrides.test.ts b/test/cli/install/nested-overrides.test.ts index 9308eb302438..4ae9f2bc9acd 100644 --- a/test/cli/install/nested-overrides.test.ts +++ b/test/cli/install/nested-overrides.test.ts @@ -2,10 +2,11 @@ import { file, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync, realpathSync } from "fs"; import { rm } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/npmrc.test.ts b/test/cli/install/npmrc.test.ts index 7b0675b1c5c4..e53e5d67a207 100644 --- a/test/cli/install/npmrc.test.ts +++ b/test/cli/install/npmrc.test.ts @@ -1,12 +1,13 @@ import { write } from "bun"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { rm } from "fs/promises"; -import { VerdaccioRegistry, bunExe, bunEnv as env, isIPv6, tempDir } from "harness"; +import { bunExe, bunEnv as env, isIPv6, tempDir } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; const { iniInternals } = require("bun:internal-for-testing"); const { loadNpmrc } = iniInternals; -var registry = new VerdaccioRegistry(); +var registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/public-hoist-pattern.test.ts b/test/cli/install/public-hoist-pattern.test.ts index 6a1c5d4a4b67..978a73dc399a 100644 --- a/test/cli/install/public-hoist-pattern.test.ts +++ b/test/cli/install/public-hoist-pattern.test.ts @@ -1,10 +1,11 @@ import { spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { readlinkSync } from "fs"; -import { VerdaccioRegistry, bunEnv, bunExe, readdirSorted, runBunInstall } from "harness"; +import { bunEnv, bunExe, readdirSorted, runBunInstall } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -const registry = new VerdaccioRegistry(); +const registry = new TestRegistry(); beforeAll(async () => { await registry.start(); diff --git a/test/cli/install/registry/packages/.gitignore b/test/cli/install/registry/packages/.gitignore index 9a465fccd1a9..b36854dd700d 100644 --- a/test/cli/install/registry/packages/.gitignore +++ b/test/cli/install/registry/packages/.gitignore @@ -1,4 +1,5 @@ -# Packages that publish tests write into this directory at run time; never commit them. +# Packages that verdaccio wrote into this directory when the publish tests ran on it. A checkout from that time +# can still have them. They are not fixtures: never commit them. /dry-run-1/ /dry-run-2/ /otp-pkg-1/ diff --git a/test/cli/install/registry/packages/create-native-binlink-packages.ts b/test/cli/install/registry/packages/create-native-binlink-packages.ts index 9d915b6eb762..a24b026165a2 100644 --- a/test/cli/install/registry/packages/create-native-binlink-packages.ts +++ b/test/cli/install/registry/packages/create-native-binlink-packages.ts @@ -93,7 +93,7 @@ await $`cp ${join(targetPkgDir, "bin", "main.js")} ${join(targetTarDir, "bin")}/ await mkdir(join(packagesDir, "test-native-binlink-target"), { recursive: true }); await $`cd ${targetPkgDir} && tar -czf ${join(packagesDir, "test-native-binlink-target", "test-native-binlink-target-1.0.0.tgz")} package`; -// Create package.json for verdaccio registry with proper integrity hashes +// Create the packument that the registry serves, with proper integrity hashes for (const pkgName of ["test-native-binlink", "test-native-binlink-target"]) { const version = "1.0.0"; const tarballName = `${pkgName}-${version}.tgz`; diff --git a/test/cli/install/registry/verdaccio.yaml b/test/cli/install/registry/verdaccio.yaml deleted file mode 100644 index 6d12f1744460..000000000000 --- a/test/cli/install/registry/verdaccio.yaml +++ /dev/null @@ -1,211 +0,0 @@ -# -# This is the default configuration file. It allows all users to do anything, -# please read carefully the documentation and best practices to -# improve security. -# -# Look here for more config file examples: -# https://github.com/verdaccio/verdaccio/tree/5.x/conf -# -# Read about the best practices -# https://verdaccio.org/docs/best - -# path to a directory with all packages -storage: ./packages -# path to a directory with plugins to include -plugins: ./plugins - -# https://verdaccio.org/docs/webui -web: - title: Verdaccio - # comment out to disable gravatar support - # gravatar: false - # by default packages are ordercer ascendant (asc|desc) - # sort_packages: asc - # convert your UI to the dark side - # darkMode: true - # html_cache: true - # by default all features are displayed - # login: true - # showInfo: true - # showSettings: true - # In combination with darkMode you can force specific theme - # showThemeSwitch: true - # showFooter: true - # showSearch: true - # showRaw: true - # showDownloadTarball: true - # HTML tags injected after manifest - # scriptsBodyAfter: - # - '' - # HTML tags injected before ends - # metaScripts: - # - '' - # - '' - # - '' - # HTML tags injected first child at - # bodyBefore: - # - '
html before webpack scripts
' - # Public path for template manifest scripts (only manifest) - # publicPath: http://somedomain.org/ - -# https://verdaccio.org/docs/configuration#authentication -auth: - htpasswd: - file: ./htpasswd -# Maximum amount of users allowed to register, defaults to "+inf". -# You can set this to -1 to disable registration. -# max_users: 1000 -# Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt". -# algorithm: bcrypt # by default is crypt, but is recommended use bcrypt for new installations -# Rounds number for "bcrypt", will be ignored for other algorithms. -# rounds: 10 - -# https://verdaccio.org/docs/configuration#uplinks -# a list of other known repositories we can talk to -uplinks: - npmjs: - url: https://registry.npmjs.org/ - -# Learn how to protect your packages -# https://verdaccio.org/docs/protect-your-dependencies/ -# https://verdaccio.org/docs/configuration#packages -packages: - "@needs-auth/*": - access: $authenticated - publish: $authenticated - unpublish: $authenticated - - "@secret/*": - access: $authenticated - publish: $authenticated - unpublish: $authenticated - - "@*/*": - # scoped packages - access: $all - publish: $all - unpublish: $all - # proxy: npmjs - - "**": - # allow all users (including non-authenticated users) to read and - # publish all packages - # - # you can specify usernames/groupnames (depending on your auth plugin) - # and three keywords: "$all", "$anonymous", "$authenticated" - access: $all - - # allow all known users to publish/publish packages - # (anyone can register by default, remember?) - publish: $all - unpublish: $all - - # if package is not available locally, proxy requests to 'npmjs' registry - # proxy: npmjs - -# To improve your security configuration and avoid dependency confusion -# consider removing the proxy property for private packages -# https://verdaccio.org/docs/best#remove-proxy-to-increase-security-at-private-packages - -# https://verdaccio.org/docs/configuration#server -# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections. -# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout. -# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough. -# server: -# keepAliveTimeout: 60 -# Allow `req.ip` to resolve properly when Verdaccio is behind a proxy or load-balancer -# See: https://expressjs.com/en/guide/behind-proxies.html -# trustProxy: '127.0.0.1' - -# https://verdaccio.org/docs/configuration#offline-publish -# publish: -# allow_offline: false - -# https://verdaccio.org/docs/configuration#url-prefix -# url_prefix: /verdaccio/ -# VERDACCIO_PUBLIC_URL='https://somedomain.org'; -# url_prefix: '/my_prefix' -# // url -> https://somedomain.org/my_prefix/ -# VERDACCIO_PUBLIC_URL='https://somedomain.org'; -# url_prefix: '/' -# // url -> https://somedomain.org/ -# VERDACCIO_PUBLIC_URL='https://somedomain.org/first_prefix'; -# url_prefix: '/second_prefix' -# // url -> https://somedomain.org/second_prefix/' - -# https://verdaccio.org/docs/configuration#security -# security: -# api: -# legacy: true -# jwt: -# sign: -# expiresIn: 29d -# verify: -# someProp: [value] -# web: -# sign: -# expiresIn: 1h # 1 hour by default -# verify: -# someProp: [value] - -# https://verdaccio.org/docs/configuration#user-rate-limit -userRateLimit: - windowMs: 1000 - max: 10000 - -# https://verdaccio.org/docs/configuration#max-body-size -# max_body_size: 10mb - -# https://verdaccio.org/docs/configuration#listen-port -# listen: -# - localhost:4873 # default value -# - http://localhost:4873 # same thing -# - 0.0.0.0:4873 # listen on all addresses (INADDR_ANY) -# - https://example.org:4873 # if you want to use https -# - "[::1]:4873" # ipv6 -# - unix:/tmp/verdaccio.sock # unix socket - -# The HTTPS configuration is useful if you do not consider use a HTTP Proxy -# https://verdaccio.org/docs/configuration#https -# https: -# key: ./path/verdaccio-key.pem -# cert: ./path/verdaccio-cert.pem -# ca: ./path/verdaccio-csr.pem - -# https://verdaccio.org/docs/configuration#proxy -# http_proxy: http://something.local/ -# https_proxy: https://something.local/ - -# https://verdaccio.org/docs/configuration#notifications -# notify: -# method: POST -# headers: [{ "Content-Type": "application/json" }] -# endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken -# content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}' - -# middlewares: -# audit: -# enabled: true - -# https://verdaccio.org/docs/logger -# log settings -log: { type: stdout, format: pretty, level: http } -#experiments: -# # support for npm token command -# token: false -# # disable writing body size to logs, read more on ticket 1912 -# bytesin_off: false -# # enable tarball URL redirect for hosting tarball with a different server, the tarball_url_redirect can be a template string -# tarball_url_redirect: 'https://mycdn.com/verdaccio/${packageName}/${filename}' -# # the tarball_url_redirect can be a function, takes packageName and filename and returns the url, when working with a js configuration file -# tarball_url_redirect(packageName, filename) { -# const signedUrl = // generate a signed url -# return signedUrl; -# } - -# translate your registry, api i18n not available yet -# i18n: -# list of the available translations https://github.com/verdaccio/verdaccio/blob/master/packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md -# web: en-US - -_debug: true diff --git a/test/cli/update_interactive_formatting.test.ts b/test/cli/update_interactive_formatting.test.ts index 13c23db5bfcd..5ac87fc573a6 100644 --- a/test/cli/update_interactive_formatting.test.ts +++ b/test/cli/update_interactive_formatting.test.ts @@ -1,16 +1,16 @@ import { dlopen, FFIType } from "bun:ffi"; import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { closeSync, createReadStream } from "fs"; -import { bunEnv, bunExe, isMusl, isWindows, tempDir, VerdaccioRegistry } from "harness"; +import { bunEnv, bunExe, isMusl, isWindows, tempDir } from "harness"; import { join } from "path"; +import { TestRegistry } from "registry"; -let registry: VerdaccioRegistry; +let registry: TestRegistry; let registryUrl: string; -beforeAll(async () => { - registry = new VerdaccioRegistry(); +beforeAll(() => { + registry = new TestRegistry().start(); registryUrl = registry.registryUrl(); - await registry.start(); }); afterAll(() => { diff --git a/test/flaky-tests.txt b/test/flaky-tests.txt index 5119860c5825..c3ed07a7252a 100644 --- a/test/flaky-tests.txt +++ b/test/flaky-tests.txt @@ -38,7 +38,6 @@ test/cli/install/bun-update.test.ts # 2 of 80 builds; alpine, ubuntu test/cli/install/bun-workspaces.test.ts # 1 of 80 builds; debian; timeout test/cli/install/config-precedence.test.ts # 4 of 80 builds test/cli/install/frozen-lockfile-missing-workspace.test.ts # 2 of 80 builds; mac, debian -test/cli/install/frozen-lockfile-pruned.test.ts # 3 of 80 builds; alpine, debian; Verdaccio exited with code 2 before the test ran (alpine aarch64) test/cli/install/hoist.test.ts # 1 of 80 builds; debian test/cli/install/migration/complex-workspace.test.ts # 1 of 80 builds; mac; error: Failed to install test/cli/install/migration/migrate.test.ts # 6 of 80 builds; parallel batch; failed in the parallel batch diff --git a/test/harness.ts b/test/harness.ts index 60fffcbc865c..65976a70c673 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -6,12 +6,12 @@ */ import * as numeric from "_util/numeric.ts"; -import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; +import { gc as bunGC, sleepSync, spawnSync, unsafe, which } from "bun"; import { heapStats } from "bun:jsc"; import { beforeAll, describe, expect } from "bun:test"; -import { ChildProcess, execSync, fork } from "child_process"; -import { readdir, rm, writeFile } from "fs/promises"; -import fs, { closeSync, openSync, rmSync } from "node:fs"; +import { execSync } from "child_process"; +import { readdir, writeFile } from "fs/promises"; +import fs, { closeSync, openSync } from "node:fs"; import os from "node:os"; import { dirname, isAbsolute, join } from "path"; @@ -1908,157 +1908,6 @@ export function textLockfile(version: number, pkgs: any): string { }); } -export class VerdaccioRegistry { - port: number; - process: ChildProcess | undefined; - configPath: string; - packagesPath: string; - users: Record = {}; - - constructor(opts?: { configPath?: string; packagesPath?: string; verbose?: boolean }) { - this.port = randomPort(); - this.configPath = opts?.configPath ?? join(import.meta.dir, "cli", "install", "registry", "verdaccio.yaml"); - this.packagesPath = opts?.packagesPath ?? join(import.meta.dir, "cli", "install", "registry", "packages"); - } - - async start(silent: boolean = true) { - await rm(join(dirname(this.configPath), "htpasswd"), { force: true }); - // Bind the IPv4 loopback explicitly: a bare port makes verdaccio listen on - // whatever `localhost` resolves to, which is `::1` on hosts that list it first, - // while the install client connects to 127.0.0.1 and every request is refused. - const listen = `127.0.0.1:${this.port}`; - this.process = fork(require.resolve("verdaccio/bin/verdaccio"), ["-c", this.configPath, "-l", listen], { - silent, - // Prefer using a release build of Bun since it's faster - execPath: isCI ? bunExe() : Bun.which("bun") || bunExe(), - env: { - ...(bunEnv as any), - NODE_NO_WARNINGS: "1", - }, - }); - - this.process.stderr?.on("data", data => { - console.error(`[verdaccio] stderr: ${data}`); - }); - - const started = Promise.withResolvers(); - - this.process.on("error", error => { - console.error(`Failed to start verdaccio: ${error}`); - started.reject(error); - }); - - this.process.on("exit", (code, signal) => { - if (code !== 0) { - console.error(`Verdaccio exited with code ${code} and signal ${signal}`); - } else { - console.log("Verdaccio exited successfully"); - } - }); - - this.process.on("message", (message: { verdaccio_started: boolean }) => { - if (message.verdaccio_started) { - started.resolve(); - } - }); - - await started.promise; - } - - registryUrl() { - return `http://localhost:${this.port}/`; - } - - stop() { - rmSync(join(dirname(this.configPath), "htpasswd"), { force: true }); - this.process?.kill(0); - } - - /** - * returns auth token - */ - async generateUser(username: string, password: string): Promise { - if (this.users[username]) { - throw new Error(`User ${username} already exists`); - } else this.users[username] = password; - - const url = `http://localhost:${this.port}/-/user/org.couchdb.user:${username}`; - const user = { - name: username, - password: password, - email: `${username}@example.com`, - }; - - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(user), - }); - - if (response.ok) { - const data = await response.json(); - return data.token; - } - - throw new Error("Failed to create user:", response.statusText); - } - - async authBunfig(user: string) { - const authToken = await this.generateUser(user, user); - return Bun.TOML.stringify({ - install: { - cache: false, - registry: { url: `http://localhost:${this.port}/`, token: authToken }, - }, - }); - } - - async createTestDir( - opts: { bunfigOpts?: BunfigOpts; files?: DirectoryTree | string } = { - bunfigOpts: { linker: "hoisted" }, - files: {}, - }, - ) { - await rm(join(dirname(this.configPath), "htpasswd"), { force: true }); - await rm(join(this.packagesPath, "private-pkg-dont-touch"), { force: true }); - const packageDir = tempDir("verdaccio-test-", opts.files ?? {}); - const packageJson = join(packageDir, "package.json"); - await this.writeBunfig(packageDir, opts.bunfigOpts); - this.users = {}; - return { packageDir: String(packageDir), packageJson }; - } - - async writeBunfig(dir: string, opts: BunfigOpts = {}) { - await write( - join(dir, "bunfig.toml"), - Bun.TOML.stringify({ - install: { - cache: join(dir, ".bun-cache"), - saveTextLockfile: opts.saveTextLockfile, - registry: opts.npm ? undefined : this.registryUrl(), - linker: opts.linker, - globalStore: opts.globalStore, - publicHoistPattern: opts.publicHoistPattern, - hoistPattern: opts.hoistPattern, - hoist: opts.hoist, - }, - }), - ); - } -} - -type BunfigOpts = { - saveTextLockfile?: boolean; - npm?: boolean; - linker?: "isolated" | "hoisted"; - globalStore?: boolean; - publicHoistPattern?: string | string[]; - hoistPattern?: string | string[]; - hoist?: boolean; -}; - export async function readdirSorted(path: string): Promise { const results = await readdir(path); results.sort(); diff --git a/test/package.json b/test/package.json index b9bc51f5a99d..8dee8b973c50 100644 --- a/test/package.json +++ b/test/package.json @@ -29,7 +29,6 @@ "@swc/core": "1.3.38", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", - "@verdaccio/config": "6.0.0-6-next.76", "acorn": "8.15.0", "ansi-regex": "6.1.0", "astro": "5.5.5", @@ -104,7 +103,6 @@ "unzipper": "0.12.3", "uuid": "11.1.0", "v8-heapsnapshot": "1.3.1", - "verdaccio": "6.0.0", "@vitest/coverage-v8": "4.1.9", "vitest": "4.1.9", "webpack": "5.88.0", diff --git a/test/packages/registry/cli.ts b/test/packages/registry/cli.ts new file mode 100644 index 000000000000..a23e93b4c181 --- /dev/null +++ b/test/packages/registry/cli.ts @@ -0,0 +1,72 @@ +#!/usr/bin/env bun +import { resolve } from "node:path"; +import { parseArgs } from "node:util"; +import { Registry } from "./src/registry.ts"; + +const usage = `Usage: bun cli.ts [options] + + --storage packages to serve: /package.json and the tarballs + --port default 4873, 0 takes a free port + --hostname
default 127.0.0.1 + --public-url base of the tarball URLs, default: the origin of each request + --user create a user and print a token for it, repeatable + --restricted packages that only a logged in user can read, repeatable (@scope/*) + --verbose print each request +`; + +/** Starts the registry that the arguments describe. Throws when it refuses one of them. */ +function run() { + const { values } = parseArgs({ + options: { + storage: { type: "string" }, + port: { type: "string", default: "4873" }, + hostname: { type: "string" }, + "public-url": { type: "string" }, + user: { type: "string", multiple: true, default: [] }, + restricted: { type: "string", multiple: true, default: [] }, + verbose: { type: "boolean", default: false }, + help: { type: "boolean", short: "h", default: false }, + }, + }); + + if (values.help) { + console.log(usage); + return; + } + + // Only digits: Number() makes 0 of "" and of " ", and it takes "0x50" and "1e3". + const port = /^[0-9]+$/.test(values.port) ? Number(values.port) : NaN; + if (!(port <= 65535)) { + throw new Error(`--port must be a number from 0 to 65535, got "${values.port}"`); + } + + const registry = new Registry({ + storage: values.storage === undefined ? undefined : resolve(values.storage), + port, + hostname: values.hostname, + publicUrl: values["public-url"], + access: Object.fromEntries(values.restricted.map(pattern => [pattern, { read: "authenticated" as const }])), + intercept: values.verbose + ? request => console.log(`${request.method} ${new URL(request.url).pathname}`) + : undefined, + }); + + const tokens: string[] = []; + for (const entry of values.user) { + const colon = entry.indexOf(":"); + if (colon <= 0) throw new Error(`--user must be name:password, got "${entry}"`); + const user = registry.auth.addUser(entry.slice(0, colon), entry.slice(colon + 1)); + tokens.push(`token for ${user.name}: ${registry.auth.createToken(user).token}`); + } + + registry.start(); + for (const token of tokens) console.log(token); + console.log(`registry: ${registry.url}`); +} + +try { + run(); +} catch (error) { + console.error(`${error instanceof Error ? error.message : error}\n\n${usage}`); + process.exit(1); +} diff --git a/test/packages/registry/index.ts b/test/packages/registry/index.ts new file mode 100644 index 000000000000..262dcdcc0115 --- /dev/null +++ b/test/packages/registry/index.ts @@ -0,0 +1,38 @@ +export { Advisories, type Advisory } from "./src/advisories.ts"; +export { + Auth, + type Credentials, + type Token, + type TokenOptions, + type TwoFactorMode, + type User, + type UserOptions, + type WebSession, +} from "./src/auth.ts"; +export { RegistryError } from "./src/http.ts"; +export { + isValidRange, + isValidTag, + isValidVersion, + parsePackageName, + validateNewPackageName, + type PackageName, +} from "./src/names.ts"; +export { + Packages, + type Access, + type AccessRule, + type AccessRules, + type PublishResult, + type ReadPolicy, + type StoredPackage, + type WritePolicy, +} from "./src/packages.ts"; +export { + abbreviatedContentType, + type Dist, + type Human, + type Packument, + type VersionDocument, +} from "./src/packument.ts"; +export { Registry, type RecordedRequest, type RegistryOptions } from "./src/registry.ts"; diff --git a/test/packages/registry/package.json b/test/packages/registry/package.json new file mode 100644 index 000000000000..2f06ca713a49 --- /dev/null +++ b/test/packages/registry/package.json @@ -0,0 +1,14 @@ +{ + "name": "registry", + "version": "0.0.0", + "private": true, + "description": "An npm registry on Bun.serve, for the tests of a package manager.", + "type": "module", + "main": "./index.ts", + "exports": { + ".": "./index.ts" + }, + "bin": { + "registry": "./cli.ts" + } +} diff --git a/test/packages/registry/src/advisories.ts b/test/packages/registry/src/advisories.ts new file mode 100644 index 000000000000..2cb6481e0d53 --- /dev/null +++ b/test/packages/registry/src/advisories.ts @@ -0,0 +1,57 @@ +/** One entry of the answer of `POST /-/npm/v1/security/advisories/bulk`. */ +export interface Advisory { + id: number; + url: string; + title: string; + severity: "info" | "low" | "moderate" | "high" | "critical"; + vulnerable_versions: string; + cwe: string[]; + cvss: { score: number; vectorString: string | null }; +} + +export class Advisories { + readonly #byPackage = new Map(); + #nextId = 1000000; + + /** Records an advisory against a package. Fields that are left out get the values of a typical advisory. */ + add(name: string, advisory: Partial & { vulnerable_versions: string }): Advisory { + const id = advisory.id ?? this.#nextId++; + // Not a spread of `advisory`: a field that is there with the value undefined must not remove its default. + const entry: Advisory = { + id, + url: advisory.url ?? `https://github.com/advisories/GHSA-${id}`, + title: advisory.title ?? `Vulnerability in ${name}`, + severity: advisory.severity ?? "high", + vulnerable_versions: advisory.vulnerable_versions, + cwe: advisory.cwe ?? [], + cvss: advisory.cvss ?? { score: 0, vectorString: null }, + }; + const advisories = this.#byPackage.get(name); + if (advisories) advisories.push(entry); + else this.#byPackage.set(name, [entry]); + return entry; + } + + clear() { + this.#byPackage.clear(); + } + + /** + * The bulk answer for a request of the form `{ "": ["", ...] }`. A package appears only when an + * advisory covers one of the versions that the client listed. + */ + lookup(request: Record): Record { + const found: Record = {}; + for (const [name, versions] of Object.entries(request)) { + const advisories = this.#byPackage.get(name); + if (!advisories || !Array.isArray(versions)) continue; + const matching = advisories.filter(advisory => + versions.some( + version => typeof version === "string" && Bun.semver.satisfies(version, advisory.vulnerable_versions), + ), + ); + if (matching.length > 0) found[name] = matching; + } + return found; + } +} diff --git a/test/packages/registry/src/auth.ts b/test/packages/registry/src/auth.ts new file mode 100644 index 000000000000..d566efe1bba2 --- /dev/null +++ b/test/packages/registry/src/auth.ts @@ -0,0 +1,221 @@ +import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; +import { RegistryError } from "./http.ts"; + +/** + * Two-factor modes of the registry. + * - `auth-only`: a one-time password is needed to log in and to create a token. + * - `auth-and-writes`: it is also needed for each publish, unpublish, deprecate, and change of `latest`. + */ +export type TwoFactorMode = "auth-only" | "auth-and-writes"; + +export interface UserOptions { + email?: string; + fullname?: string; + tfa?: TwoFactorMode; + /** + * The one-time passwords this user can present. A real authenticator derives them from the time. A test needs + * to know them in advance. Each code works any number of times. + */ + otp?: string[]; +} + +export interface User { + name: string; + email: string; + fullname: string; + passwordHash: Buffer; + tfa: TwoFactorMode | null; + otp: string[]; + created: Date; + updated: Date; +} + +export interface TokenOptions { + readonly?: boolean; + /** An automation token passes the two-factor check on a write. */ + automation?: boolean; + cidr_whitelist?: string[] | null; +} + +export interface Token { + token: string; + /** Hex sha512 of `token`. The registry shows this, never the token, after creation. */ + key: string; + user: string; + readonly: boolean; + automation: boolean; + cidr_whitelist: string[] | null; + created: Date; + updated: Date; +} + +export type Credentials = + /** No `Authorization` header. */ + | { kind: "none" } + /** A header that names no user of this registry: an unknown token, a wrong password, a scheme it does not know. */ + | { kind: "invalid" } + | { kind: "user"; user: User; via: "basic" | "bearer"; token: Token | null }; + +/** The state of a login or a one-time password that the user completes in a browser. */ +export interface WebSession { + id: string; + kind: "login" | "otp"; + /** The user that asked for the one-time password. A login session has none before it is approved. */ + user: string | null; + /** The value that the `doneUrl` hands to the client. Null while the user has not approved. */ + result: string | null; + polls: number; +} + +const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +/** `npm_` and 36 characters, the form of a token of registry.npmjs.org. */ +function generateToken(): string { + let token = "npm_"; + // 248 is the largest multiple of 62 below 256. A byte above it would favor the first characters. + for (const byte of randomBytes(96)) { + if (byte >= 248) continue; + token += alphabet[byte % 62]; + if (token.length === 40) return token; + } + return generateToken(); +} + +function hashPassword(password: string): Buffer { + return new Bun.CryptoHasher("sha256").update(password).digest(); +} + +export class Auth { + readonly users = new Map(); + readonly tokens = new Map(); + readonly sessions = new Map(); + + addUser(name: string, password: string, options: UserOptions = {}): User { + if (this.users.has(name)) throw new RegistryError(409, `user ${name} already exists`); + if (!/^[a-z0-9][a-z0-9._~-]*$/i.test(name) || name.length > 214) { + throw new RegistryError(400, "Name may not contain non-url-safe chars"); + } + if (typeof password !== "string" || password.length === 0) { + throw new RegistryError(400, "A password is required"); + } + const now = new Date(); + const user: User = { + name, + email: options.email ?? `${name}@example.com`, + fullname: options.fullname ?? "", + passwordHash: hashPassword(password), + tfa: options.tfa ?? null, + otp: options.otp ?? [], + created: now, + updated: now, + }; + this.users.set(name, user); + return user; + } + + verifyPassword(user: User, password: string): boolean { + return timingSafeEqual(user.passwordHash, hashPassword(password)); + } + + setPassword(user: User, password: string) { + user.passwordHash = hashPassword(password); + user.updated = new Date(); + } + + createToken(user: string | User, options: TokenOptions = {}): Token { + const name = typeof user === "string" ? user : user.name; + if (!this.users.has(name)) throw new RegistryError(404, `user ${name} does not exist`); + const token = generateToken(); + const now = new Date(); + const entry: Token = { + token, + key: new Bun.CryptoHasher("sha512").update(token).digest("hex"), + user: name, + readonly: options.readonly ?? false, + automation: options.automation ?? false, + cidr_whitelist: options.cidr_whitelist ?? null, + created: now, + updated: now, + }; + this.tokens.set(token, entry); + return entry; + } + + /** Removes a token by its value or by its key. Returns false when there is no such token. */ + revokeToken(tokenOrKey: string, owner?: string): boolean { + for (const [value, entry] of this.tokens) { + if (value !== tokenOrKey && entry.key !== tokenOrKey) continue; + if (owner !== undefined && entry.user !== owner) continue; + this.tokens.delete(value); + return true; + } + return false; + } + + tokensOf(user: string): Token[] { + return [...this.tokens.values()].filter(token => token.user === user); + } + + /** Reads the `Authorization` header. The registry accepts `Bearer ` and `Basic `. */ + credentials(request: Request): Credentials { + const header = request.headers.get("authorization"); + if (header === null || header.trim().length === 0) return { kind: "none" }; + const space = header.indexOf(" "); + if (space === -1) return { kind: "invalid" }; + const scheme = header.slice(0, space).toLowerCase(); + const value = header.slice(space + 1).trim(); + + if (scheme === "bearer") { + const token = this.tokens.get(value); + const user = token && this.users.get(token.user); + return user ? { kind: "user", user, via: "bearer", token } : { kind: "invalid" }; + } + + if (scheme === "basic") { + const decoded = Buffer.from(value, "base64").toString("utf8"); + const colon = decoded.indexOf(":"); + if (colon === -1) return { kind: "invalid" }; + const user = this.users.get(decoded.slice(0, colon)); + if (!user || !this.verifyPassword(user, decoded.slice(colon + 1))) return { kind: "invalid" }; + return { kind: "user", user, via: "basic", token: null }; + } + + return { kind: "invalid" }; + } + + isValidOtp(user: User, otp: string | null): boolean { + if (otp === null) return false; + if (user.otp.includes(otp)) return true; + // The result of a web session that this user approved counts as a one-time password, once. + for (const [id, session] of this.sessions) { + if (session.kind === "otp" && session.user === user.name && session.result === otp) { + this.sessions.delete(id); + return true; + } + } + return false; + } + + openSession(kind: WebSession["kind"], user: string | null): WebSession { + const session: WebSession = { id: randomUUID(), kind, user, result: null, polls: 0 }; + this.sessions.set(session.id, session); + return session; + } + + /** + * Does what the user does in the browser. A login session gets a new token of `user`. A one-time password + * session gets a code that is good for one write. + */ + approveSession(id: string, user?: string): WebSession { + const session = this.sessions.get(id); + if (!session) throw new RegistryError(404, `no web session ${id}`); + if (session.kind === "login") { + if (user === undefined) throw new RegistryError(400, "a login session needs the user that logs in"); + session.user = user; + session.result = this.createToken(user).token; + } else { + session.result = randomUUID(); + } + return session; + } +} diff --git a/test/packages/registry/src/http.ts b/test/packages/registry/src/http.ts new file mode 100644 index 000000000000..c52ad761fe50 --- /dev/null +++ b/test/packages/registry/src/http.ts @@ -0,0 +1,231 @@ +import { brotliCompressSync, constants as zlib } from "node:zlib"; + +/** + * A failure that the registry reports to the client. The default body is `{"error": message}`, the shape that + * `npm-registry-fetch` and bun read. Pass `body` for the endpoints that answer with another shape, and + * `body: undefined` for the ones that answer with no body. + */ +export class RegistryError extends Error { + status: number; + body: unknown; + headers: Record; + + constructor(status: number, message: string, options: { body?: unknown; headers?: Record } = {}) { + super(message); + this.name = "RegistryError"; + this.status = status; + this.body = "body" in options ? options.body : { error: message }; + this.headers = options.headers ?? {}; + } +} + +export const notFound = () => new RegistryError(404, "Not found"); + +/** The answer for a path under `/-/` that no service owns. */ +export const resourceNotFound = (pathname: string) => + new RegistryError(404, `${pathname} does not exist`, { + body: { code: "ResourceNotFound", message: `${pathname} does not exist` }, + }); + +export const methodNotAllowed = (method: string, allow: string[]) => + new RegistryError(405, `${method} is not allowed`, { + body: { code: "MethodNotAllowedError", message: `${method} is not allowed` }, + headers: { allow: allow.join(", ") }, + }); + +export type Encoding = "br" | "gzip"; + +export type Bytes = Uint8Array; + +export interface Body { + bytes: Bytes; + /** Hex md5 of `bytes`. The registry uses it as the entity tag. */ + md5: string; + encoded: Partial>; +} + +const encoder = new TextEncoder(); + +export function bodyOf(value: unknown): Body { + const bytes = encoder.encode(JSON.stringify(value)); + return { bytes, md5: new Bun.CryptoHasher("md5").update(bytes).digest("hex"), encoded: {} }; +} + +/** + * The registry does not encode a short body: 26 bytes came back as they are, 63 bytes came back encoded. Where + * between the two it starts is not known. This is the value that is used here. + */ +const minimumCompressedSize = 48; + +/** + * The registry compresses with brotli when the client accepts it, else with gzip. It does not use deflate or zstd. + */ +export function negotiateEncoding(acceptEncoding: string | null): Encoding | null { + if (!acceptEncoding) return null; + let gzip = false; + for (const part of acceptEncoding.split(",")) { + const [coding, ...parameters] = part.split(";").map(piece => piece.trim().toLowerCase()); + const q = parameters.find(parameter => parameter.startsWith("q=")); + if (q !== undefined && !(Number.parseFloat(q.slice(2)) > 0)) continue; + if (coding === "br") return "br"; + if (coding === "gzip" || coding === "x-gzip") gzip = true; + } + return gzip ? "gzip" : null; +} + +function encode(body: Body, encoding: Encoding): Bytes { + return (body.encoded[encoding] ??= + encoding === "br" + ? new Uint8Array(brotliCompressSync(body.bytes, { params: { [zlib.BROTLI_PARAM_QUALITY]: 4 } })) + : Bun.gzipSync(body.bytes, { level: 6 })); +} + +/** Weak comparison of RFC 9110 section 8.8.3.2: `W/"a"` matches `"a"`. */ +export function matchesEntityTag(header: string | null, md5: string): boolean { + if (!header) return false; + for (const candidate of header.split(",")) { + let tag = candidate.trim(); + if (tag === "*") return true; + if (tag.startsWith("W/")) tag = tag.slice(2); + if (tag === `"${md5}"`) return true; + } + return false; +} + +export interface SendOptions { + status?: number; + headers?: Record; + /** + * Treat the body as a stored object, as the registry does for a packument: send the entity tag, answer a + * matching `If-None-Match` with 304, and answer a `Range` with the part that it names. + */ + conditional?: boolean; +} + +/** Reads `Range: bytes=-`. The registry answers a request for more than one range with all of it. */ +export function parseRange( + header: string | null, + size: number, +): { start: number; end: number } | "unsatisfiable" | null { + const match = header?.match(/^bytes=(\d*)-(\d*)$/); + if (!match || (match[1] === "" && match[2] === "")) return null; + let start: number; + let end: number; + if (match[1] === "") { + const suffix = Number(match[2]); + if (suffix === 0) return "unsatisfiable"; + start = Math.max(0, size - suffix); + end = size - 1; + } else { + start = Number(match[1]); + end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1); + } + return start > end || start >= size ? "unsatisfiable" : { start, end }; +} + +export const rangeNotSatisfiable = { error: "Requested range not satisfiable" }; + +/** Sends a JSON body the way the registry does: compact, compressed on request, with an md5 entity tag. */ +export function send(request: Request, body: Body, options: SendOptions = {}): Response { + const headers = new Headers(options.headers); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + + if (options.conditional) { + // `If-Modified-Since` has no effect on the registry. Only the entity tag selects a 304. + if (matchesEntityTag(request.headers.get("if-none-match"), body.md5)) { + headers.delete("content-type"); + headers.delete("vary"); + headers.set("etag", `"${body.md5}"`); + return new Response(null, { status: 304, headers }); + } + } + + if (options.conditional) { + const range = parseRange(request.headers.get("range"), body.bytes.byteLength); + if (range === "unsatisfiable") { + const failed = new Headers({ "content-range": `bytes */${body.bytes.byteLength}`, "etag": `"${body.md5}"` }); + const modified = headers.get("last-modified"); + if (modified !== null) failed.set("last-modified", modified); + return sendJson(request, rangeNotSatisfiable, { status: 416, headers: Object.fromEntries(failed) }); + } + if (range !== null) { + // A part of the body is a part of the body as it is stored, so it is never encoded. + const part = body.bytes.subarray(range.start, range.end + 1); + headers.set("content-range", `bytes ${range.start}-${range.end}/${body.bytes.byteLength}`); + headers.set("etag", `"${body.md5}"`); + return new Response(request.method === "HEAD" ? null : part, { + status: 206, + headers: request.method === "HEAD" ? withHeader(headers, "content-length", String(part.byteLength)) : headers, + }); + } + headers.set("accept-ranges", "bytes"); + } + + let bytes = body.bytes; + let encoding: Encoding | null = null; + if (bytes.byteLength >= minimumCompressedSize) { + encoding = negotiateEncoding(request.headers.get("accept-encoding")); + if (encoding) { + bytes = encode(body, encoding); + headers.set("content-encoding", encoding); + } + } + if (options.conditional) { + headers.set("etag", encoding ? `W/"${body.md5}"` : `"${body.md5}"`); + } + return new Response(request.method === "HEAD" ? null : bytes, { + status: options.status ?? 200, + headers: request.method === "HEAD" ? withHeader(headers, "content-length", String(bytes.byteLength)) : headers, + }); +} + +function withHeader(headers: Headers, name: string, value: string): Headers { + headers.set(name, value); + return headers; +} + +export function sendJson(request: Request, value: unknown, options: SendOptions = {}): Response { + return send(request, bodyOf(value), options); +} + +export function sendError(request: Request, error: RegistryError): Response { + if (error.body === undefined) return new Response(null, { status: error.status, headers: error.headers }); + return sendJson(request, error.body, { status: error.status, headers: error.headers }); +} + +/** + * Reads a JSON request body. The registry accepts `Content-Encoding: gzip`, which `npm audit` and bun use. + * + * The Content-Type must be `application/json`, character for character. A parameter such as `; charset=utf-8` is + * refused. This is the check of verdaccio, which many people run as their registry. `bun publish` must keep to + * it (src/runtime/cli/publish_command.rs), and these tests are what holds it to that. + */ +export async function readJson(request: Request): Promise { + const type = request.headers.get("content-type"); + if (type !== "application/json") { + throw new RegistryError(415, `Unsupported Media Type: expected application/json, got ${type ?? "no Content-Type"}`); + } + let bytes = await request.bytes(); + const encoding = request.headers.get("content-encoding")?.trim().toLowerCase(); + try { + if (encoding === "gzip" || encoding === "x-gzip") bytes = Bun.gunzipSync(bytes); + else if (encoding === "deflate") bytes = Bun.inflateSync(bytes); + else if (encoding && encoding !== "identity") throw new Error(encoding); + } catch { + throw new RegistryError(400, `Bad Request: cannot decode a ${encoding} body`); + } + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + } catch { + throw new RegistryError(400, "Bad Request: the body is not valid JSON"); + } +} + +const httpDate = (date: Date) => date.toUTCString(); + +export function lastModified(date: Date): string { + // An HTTP date has a resolution of one second. The registry rounds up, so that the header is never older than + // the document. + const milliseconds = date.getTime(); + return httpDate(new Date(Math.ceil(milliseconds / 1000) * 1000)); +} diff --git a/test/packages/registry/src/names.ts b/test/packages/registry/src/names.ts new file mode 100644 index 000000000000..299cd6390431 --- /dev/null +++ b/test/packages/registry/src/names.ts @@ -0,0 +1,112 @@ +import { builtinModules } from "node:module"; + +export interface PackageName { + /** `@scope/name` or `name`. */ + name: string; + /** `scope` of `@scope/name`, without the `@`. */ + scope: string | null; + /** `name` of `@scope/name`. */ + basename: string; + /** The form clients put in a URL: `@scope%2fname`. */ + escaped: string; +} + +const blockedNames = new Set(["node_modules", "favicon.ico"]); +const builtins = new Set(builtinModules.map(name => name.toLowerCase())); + +function isUrlSafe(part: string): boolean { + return encodeURIComponent(part) === part; +} + +/** + * The rules of `validate-npm-package-name` that apply to every name, old and new. + * A name that passes is also safe as a path below the storage directory: it has at most one `/`, and no part + * starts with a `.` or holds a `\`, a `:` or a NUL. + */ +export function parsePackageName(name: string): PackageName | null { + if (typeof name !== "string" || name.length === 0) return null; + if (name.trim() !== name) return null; + if (name.startsWith(".") || name.startsWith("_") || name.startsWith("-")) return null; + if (blockedNames.has(name.toLowerCase())) return null; + + if (name.startsWith("@")) { + const slash = name.indexOf("/"); + if (slash === -1) return null; + const scope = name.slice(1, slash); + const basename = name.slice(slash + 1); + if (scope.length === 0 || basename.length === 0) return null; + if (basename.startsWith(".")) return null; + if (!isUrlSafe(scope) || !isUrlSafe(basename)) return null; + return { name, scope, basename, escaped: `@${scope}%2f${basename}` }; + } + + if (!isUrlSafe(name)) return null; + return { name, scope: null, basename: name, escaped: name }; +} + +/** + * The rules that the registry adds for a name nobody has published yet. + * Returns the reason the name is refused, or null when the name is fine. + */ +export function validateNewPackageName(name: string): string | null { + const parsed = parsePackageName(name); + if (!parsed) return "name can only contain URL-friendly characters"; + if (name.length > 214) return "name can no longer contain more than 214 characters"; + if (name.toLowerCase() !== name) return "name can no longer contain capital letters"; + if (/[~'!()*]/.test(parsed.basename)) return `name can no longer contain special characters ("~'!()*")`; + if (builtins.has(name)) return `${name} is a core module name`; + return null; +} + +const numeric = "(?:0|[1-9]\\d*)"; +const identifiers = "[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*"; +const prereleaseIdentifier = `(?:${numeric}|\\d*[A-Za-z-][0-9A-Za-z-]*)`; + +const versionPattern = new RegExp( + `^${numeric}\\.${numeric}\\.${numeric}(?:-${prereleaseIdentifier}(?:\\.${prereleaseIdentifier})*)?(?:\\+${identifiers})?$`, +); + +/** Strict semver 2.0.0, the form the registry stores. `v1.0.0` and `1.0` do not pass. */ +export function isValidVersion(version: unknown): version is string { + return typeof version === "string" && version.length <= 256 && versionPattern.test(version); +} + +/** `1.0.0` of `1.0.0+build.5`. Two versions that differ only in the build metadata are the same version. */ +export function withoutBuildMetadata(version: string): string { + const plus = version.indexOf("+"); + return plus === -1 ? version : version.slice(0, plus); +} + +const xr = `(?:[xX*]|${numeric})`; +const partial = `[v=\\s]*${xr}(?:\\.${xr}(?:\\.${xr}(?:-?${identifiers})?(?:\\+${identifiers})?)?)?`; +const primitive = `(?:[<>]?=?|~>?|\\^)\\s*${partial}`; +const comparators = `(?:${partial}\\s+-\\s+${partial}|${primitive}(?:\\s+${primitive})*)`; +const rangePattern = new RegExp(`^\\s*(?:${comparators})?\\s*(?:\\|\\|\\s*(?:${comparators})?\\s*)*$`); + +/** The grammar of `semver.validRange`: `1`, `1.x`, `^1.2.3`, `>=1 <2`, `1.0.0 - 2.0.0`, `*`, and the empty string. */ +export function isValidRange(range: string): boolean { + return rangePattern.test(range); +} + +/** A dist-tag must not read as a version range, or `pkg@` is ambiguous. This is the rule of `npm dist-tag`. */ +export function isValidTag(tag: unknown): tag is string { + if (typeof tag !== "string" || tag.length === 0 || tag.length > 214) return false; + // An assignment to this key of an object does not store anything. + if (tag.trim() !== tag || tag === "__proto__") return false; + return !isValidRange(tag); +} + +/** Orders two versions: negative when `a` is older. Both must pass `isValidVersion`. */ +export function compareVersions(a: string, b: string): number { + return Bun.semver.order(a, b); +} + +/** The highest version of the list, or undefined when the list is empty. A prerelease ranks below its release. */ +export function highestVersion(versions: Iterable): string | undefined { + let highest: string | undefined; + for (const version of versions) { + if (!isValidVersion(version)) continue; + if (highest === undefined || compareVersions(version, highest) > 0) highest = version; + } + return highest; +} diff --git a/test/packages/registry/src/packages.ts b/test/packages/registry/src/packages.ts new file mode 100644 index 000000000000..31eff118a0f3 --- /dev/null +++ b/test/packages/registry/src/packages.ts @@ -0,0 +1,643 @@ +import { statSync } from "node:fs"; +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; +import type { Credentials, User } from "./auth.ts"; +import { RegistryError, notFound, type Body } from "./http.ts"; +import type { PackageName } from "./names.ts"; +import { + highestVersion, + isValidTag, + isValidVersion, + parsePackageName, + validateNewPackageName, + withoutBuildMetadata, +} from "./names.ts"; +import { own, tarballFilename, type Human, type Packument, type VersionDocument } from "./packument.ts"; + +export type Access = "public" | "restricted"; + +/** Who can read a package: its packument, its versions, its dist-tags and its tarballs. */ +export type ReadPolicy = "anyone" | "authenticated" | "maintainers" | string[]; +/** Who can publish, unpublish, deprecate and tag. A write always needs a user. */ +export type WritePolicy = "authenticated" | "maintainers" | string[]; + +export interface AccessRule { + read?: ReadPolicy; + write?: WritePolicy; +} + +/** + * Rules by name pattern, where `*` stands for any run of characters: `@scope/*`, `internal-*`, `*`. The first + * pattern that matches decides. A package that no pattern matches follows the registry defaults: everyone reads a + * public package, the maintainers read a restricted package, and the maintainers write. A package without + * maintainers, which is each package that comes from the storage directory, accepts a write from every user. + */ +export type AccessRules = Record; + +export interface StoredPackage { + name: PackageName; + document: Packument; + access: Access; + /** Tarballs by file name. A file of the storage directory is read when a client asks for it. */ + tarballs: Map; + /** Versions that were unpublished. The registry never accepts them again. */ + unpublished: Set; + /** Stands in for `time.modified` when the stored document has none. */ + modified: Date; + /** Rendered responses by form and base URL. Every change of the package empties it. */ + rendered: Map; + tarballHashes: Map; +} + +export interface PublishResult { + ok: true; + id: string; + rev: string; +} + +const day = 24 * 60 * 60 * 1000; + +function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function patternToRegExp(pattern: string): RegExp { + const source = pattern + .split(/\*+/) + .map(literal => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join(".*"); + return new RegExp(`^${source}$`); +} + +function nextRevision(current: string | undefined, document: unknown): string { + const sequence = Number.parseInt(current ?? "", 10); + const hash = new Bun.CryptoHasher("md5").update(JSON.stringify(document)).digest("hex"); + return `${Number.isFinite(sequence) ? sequence + 1 : 1}-${hash}`; +} + +const hoistedFields = [ + "description", + "homepage", + "keywords", + "repository", + "author", + "bugs", + "license", + "contributors", + "readmeFilename", +] as const; + +/** The registry keeps the first 64K of a readme. */ +const maximumReadmeLength = 64 * 1024; + +const integrityAlgorithms = ["sha512", "sha384", "sha256", "sha1"] as const; + +export class Packages { + /** Packages in memory: the ones a client published, and the ones of the storage directory that were read. */ + readonly #loaded = new Map>(); + /** Names that were published and then removed, with the time. The registry blocks them for 24 hours. */ + readonly #removed = new Map }>(); + readonly #rules: [RegExp, AccessRule][]; + /** The last write of each package. A write waits for the one before it, so that two never see the same state. */ + readonly #writes = new Map>(); + + constructor( + readonly storage: string | undefined, + rules: AccessRules = {}, + ) { + // Without this check, a path with a typing error gives a registry that answers 404 for every package. + if (storage !== undefined && !statSync(storage, { throwIfNoEntry: false })?.isDirectory()) { + throw new Error(`The storage is not a directory: ${storage}`); + } + this.#rules = Object.entries(rules).map(([pattern, rule]) => [patternToRegExp(pattern), rule]); + } + + async get(name: string): Promise { + let loading = this.#loaded.get(name); + if (loading === undefined) { + const parsed = parsePackageName(name); + if (parsed === null) return null; + loading = this.#read(parsed); + this.#loaded.set(name, loading); + // A read that failed says nothing about the package. The next request reads again. + const read = loading; + read.catch(() => { + if (this.#loaded.get(name) === read) this.#loaded.delete(name); + }); + } + return loading; + } + + async has(name: string): Promise { + return (await this.get(name)) !== null; + } + + /** + * Removes a package and every trace of it, so that a test can publish the same name and version again. This is + * not what `npm unpublish` does: see `unpublish`. + */ + delete(name: string) { + this.#loaded.set(name, Promise.resolve(null)); + this.#removed.delete(name); + } + + /** Puts a package of the storage directory back to the state on disk. */ + reset(name?: string) { + if (name === undefined) { + this.#loaded.clear(); + this.#removed.clear(); + } else { + this.#loaded.delete(name); + this.#removed.delete(name); + } + } + + /** Every package name, sorted. */ + async names(): Promise { + const names = new Set(); + if (this.storage !== undefined) { + const hasPackument = (directory: string) => Bun.file(join(directory, "package.json")).exists(); + for (const entry of await readdir(this.storage, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const directory = join(this.storage, entry.name); + if (!entry.name.startsWith("@")) { + if (await hasPackument(directory)) names.add(entry.name); + continue; + } + for (const scoped of await readdir(directory, { withFileTypes: true })) { + if (scoped.isDirectory() && (await hasPackument(join(directory, scoped.name)))) { + names.add(`${entry.name}/${scoped.name}`); + } + } + } + } + for (const name of this.#loaded.keys()) names.add(name); + const present = await Promise.all([...names].map(async name => ((await this.get(name)) ? name : null))); + return present.filter(name => name !== null).sort(); + } + + #exclusive(name: string, write: () => Promise): Promise { + const result = (this.#writes.get(name) ?? Promise.resolve()).then(write, write); + this.#writes.set( + name, + result.catch(() => {}), + ); + return result; + } + + async #read(name: PackageName): Promise { + if (this.storage === undefined) return null; + // A name is a package of the storage only when a directory has exactly that name. A file system can ignore + // the case of a name, drop a period at its end, or refuse some of its characters. The registry does not. + let directory = this.storage; + for (const part of name.name.split("/")) { + if (!(await entries(directory)).includes(part)) return null; + directory = join(directory, part); + } + const file = Bun.file(join(directory, "package.json")); + if (!(await file.exists())) return null; + let document: unknown; + try { + document = await file.json(); + } catch (error) { + throw new Error(`${file.name} is not a packument: ${error}`); + } + if (!isObject(document) || !isObject(document.versions) || !isObject(document["dist-tags"])) { + throw new Error(`${file.name} is not a packument: it needs "versions" and "dist-tags"`); + } + const stored: StoredPackage = { + name, + document: document as Packument, + access: "public", + tarballs: new Map(), + unpublished: new Set(), + modified: new Date(file.lastModified), + rendered: new Map(), + tarballHashes: new Map(), + }; + for (const version of Object.values(stored.document.versions)) { + const filename = tarballFilename(version, name.basename); + // A file name comes from a stored URL. It must not leave the directory of the package. + if (filename.includes("/") || filename.includes("\\") || filename.startsWith(".")) continue; + stored.tarballs.set(filename, Bun.file(join(directory, filename))); + } + return stored; + } + + #rule(name: string): AccessRule | undefined { + for (const [pattern, rule] of this.#rules) { + if (pattern.test(name)) return rule; + } + } + + canRead(stored: StoredPackage, credentials: Credentials): boolean { + const policy = this.#rule(stored.name.name)?.read ?? (stored.access === "public" ? "anyone" : "maintainers"); + if (policy === "anyone") return true; + if (credentials.kind !== "user") return false; + if (policy === "authenticated") return true; + if (policy === "maintainers") return isMaintainer(stored, credentials.user.name); + return policy.includes(credentials.user.name); + } + + canWrite(name: string, stored: StoredPackage | null, user: User): boolean { + const policy = this.#rule(name)?.write ?? "maintainers"; + if (policy === "authenticated") return true; + if (policy === "maintainers") { + const maintainers = stored?.document.maintainers; + return !maintainers || maintainers.length === 0 || isMaintainer(stored, user.name); + } + return policy.includes(user.name); + } + + /** + * The package for a read, or the 404 that the registry sends for a package that is not there. It sends the same + * 404 for a package that the client is not allowed to see, so that the answer does not reveal the name. + */ + async read(name: string, credentials: Credentials): Promise { + const stored = await this.get(name); + if (stored === null || !this.canRead(stored, credentials)) throw notFound(); + return stored; + } + + async #forWrite(name: PackageName, user: User): Promise { + const stored = await this.get(name.name); + if (stored === null || !this.canRead(stored, { kind: "user", user, via: "bearer", token: null })) throw notFound(); + if (!this.canWrite(name.name, stored, user)) throw forbidden(name.name); + return stored; + } + + #commit(stored: StoredPackage, now: Date): PublishResult { + const document = stored.document; + document.time = { ...document.time, modified: now.toISOString() }; + document.time.created ??= document.time.modified; + stored.modified = now; + document._id = stored.name.name; + document._rev = nextRevision(document._rev, document); + stored.rendered.clear(); + return { ok: true, id: stored.name.name, rev: document._rev }; + } + + /** `PUT /` with `_attachments`: one new version and its tarball. */ + publish(name: PackageName, body: unknown, user: User): Promise { + return this.#exclusive(name.name, async () => { + if (!isObject(body)) throw new RegistryError(400, "Bad Request: the body must be a JSON object"); + if (body.name !== name.name) { + throw new RegistryError(400, `Bad Request: the body is for "${body.name}", the URL is for "${name.name}"`); + } + const versions = isObject(body.versions) ? Object.entries(body.versions) : []; + if (versions.length !== 1) { + throw new RegistryError(400, "Bad Request: a publish holds one version"); + } + const [key, incoming] = versions[0]; + // Build metadata does not make another version. bun sends the key without it and the manifest with it. + if (!isValidVersion(key) || key !== withoutBuildMetadata(key)) { + throw new RegistryError(400, `Bad Request: "${key}" is not a valid version`); + } + if ( + !isObject(incoming) || + incoming.name !== name.name || + typeof incoming.version !== "string" || + !isValidVersion(incoming.version) || + withoutBuildMetadata(incoming.version) !== key + ) { + throw new RegistryError(400, `Bad Request: versions["${key}"] must have this name and this version`); + } + + const existing = await this.get(name.name); + if (existing !== null && !this.canRead(existing, { kind: "user", user, via: "bearer", token: null })) { + throw forbidden(name.name); + } + if (!this.canWrite(name.name, existing, user)) throw forbidden(name.name); + + const removed = this.#removed.get(name.name); + if (existing === null) { + const refusal = validateNewPackageName(name.name); + if (refusal !== null) + throw new RegistryError(400, `Bad Request: invalid package name "${name.name}": ${refusal}`); + if (removed !== undefined && Date.now() - removed.at < day) { + throw new RegistryError(403, `${name.name} cannot be republished until 24 hours have passed.`); + } + } + if ( + (existing && own(existing.document.versions, key)) || + existing?.unpublished.has(key) || + removed?.versions.has(key) + ) { + throw new RegistryError(403, `You cannot publish over the previously published versions: ${key}.`); + } + + const access = body.access ?? null; + if (access !== null && access !== "public" && access !== "restricted") { + throw new RegistryError(400, `Bad Request: access must be "public" or "restricted"`); + } + if (access === "restricted" && name.scope === null) { + throw new RegistryError(400, "Bad Request: only a scoped package can be restricted"); + } + + const tarball = readAttachment(body._attachments); + const shasum = new Bun.CryptoHasher("sha1").update(tarball).digest("hex"); + const dist = isObject(incoming.dist) ? incoming.dist : {}; + if (typeof dist.shasum === "string" && dist.shasum.toLowerCase() !== shasum) { + throw new RegistryError(400, `Bad Request: dist.shasum is ${dist.shasum}, the tarball has ${shasum}`); + } + if (typeof dist.integrity === "string") verifyIntegrity(dist.integrity, tarball); + + const tags = isObject(body["dist-tags"]) ? Object.entries(body["dist-tags"]) : []; + for (const [tag, target] of tags) { + if (!isValidTag(tag)) throw new RegistryError(400, `Bad Request: "${tag}" is not a valid dist-tag`); + if (target !== key) throw new RegistryError(400, `Bad Request: dist-tag "${tag}" must point to ${key}`); + } + + const now = new Date(); + const publisher: Human = { name: user.name, email: user.email }; + const stored: StoredPackage = existing ?? { + name, + document: { _id: name.name, name: name.name, "dist-tags": {}, versions: {}, maintainers: [publisher] }, + access: access ?? (name.scope === null ? "public" : "restricted"), + tarballs: new Map(), + unpublished: removed?.versions ?? new Set(), + modified: now, + rendered: new Map(), + tarballHashes: new Map(), + }; + if (existing === null) { + this.#loaded.set(name.name, Promise.resolve(stored)); + this.#removed.delete(name.name); + } else if (access !== null) { + stored.access = access; + } + + const filename = `${name.basename}-${key}.tgz`; + const { readme, ...manifest } = incoming as VersionDocument & { readme?: unknown }; + const version: VersionDocument = { + ...manifest, + _id: `${name.name}@${key}`, + dist: { + ...dist, + integrity: `sha512-${new Bun.CryptoHasher("sha512").update(tarball).digest("base64")}`, + shasum, + tarball: `${name.name}/-/${filename}`, + }, + _npmUser: publisher, + maintainers: stored.document.maintainers ?? [publisher], + }; + + const document = stored.document; + document.versions[key] = version; + stored.tarballs.set(filename, new Blob([tarball])); + stored.tarballHashes.delete(filename); + for (const [tag] of tags) document["dist-tags"][tag] = key; + document["dist-tags"].latest ??= key; + document.time = { ...document.time, [key]: now.toISOString() }; + + if (document["dist-tags"].latest === key) { + const hoisted: Record = document; + for (const field of hoistedFields) { + if (version[field] === undefined) delete hoisted[field]; + else hoisted[field] = version[field]; + } + const text = typeof readme === "string" ? readme : typeof body.readme === "string" ? body.readme : ""; + document.readme = text.slice(0, maximumReadmeLength); + } + return this.#commit(stored, now); + }); + } + + /** + * `PUT /` without a tarball. `npm deprecate` sends the whole packument with new `deprecated` messages, and + * `npm star` sends `users`. + */ + change(name: PackageName, body: unknown, user: User): Promise { + return this.#exclusive(name.name, async () => { + if (!isObject(body)) throw new RegistryError(400, "Bad Request: the body must be a JSON object"); + const stored = await this.get(name.name); + if (stored === null || !this.canRead(stored, { kind: "user", user, via: "bearer", token: null })) + throw notFound(); + + // Every check comes before the first change, so a request that is refused changes nothing. + const deprecations: [VersionDocument, string | undefined][] = []; + if (isObject(body.versions)) { + for (const [key, incoming] of Object.entries(body.versions)) { + const version = own(stored.document.versions, key); + if (version === undefined || !isObject(incoming)) continue; + const message = incoming.deprecated; + if (message === version.deprecated) continue; + if (message !== undefined && typeof message !== "string") { + throw new RegistryError(400, "Bad Request: a deprecation message is a string"); + } + if (!this.canWrite(name.name, stored, user)) throw forbidden(name.name); + deprecations.push([version, message]); + } + } + + let changed = deprecations.length > 0; + for (const [version, message] of deprecations) { + // An empty message takes the deprecation away. + if (!message) delete version.deprecated; + else version.deprecated = message; + } + if (isObject(body.users)) { + const users = { ...stored.document.users }; + if (own(body.users, user.name)) users[user.name] = true; + else delete users[user.name]; + stored.document.users = users; + changed = true; + } + if (!changed) return { ok: true, id: name.name, rev: stored.document._rev ?? "" }; + return this.#commit(stored, new Date()); + }); + } + + /** + * `PUT //-rev/`: the client read the packument, edited it, and sends it back. `npm unpublish ` + * removes a version this way, and `npm owner` replaces `maintainers`. + */ + replace(name: PackageName, revision: string, body: unknown, user: User): Promise { + return this.#exclusive(name.name, async () => { + if (!isObject(body)) throw new RegistryError(400, "Bad Request: the body must be a JSON object"); + const stored = await this.#forWrite(name, user); + checkRevision(stored, revision); + const document = stored.document; + + // Every check comes before the first change, so a request that is refused changes nothing. + const incomingTags = isObject(body["dist-tags"]) ? Object.entries(body["dist-tags"]) : null; + for (const [tag] of incomingTags ?? []) { + if (!isValidTag(tag)) throw new RegistryError(400, `Bad Request: "${tag}" is not a valid dist-tag`); + } + let maintainers: Human[] | null = null; + if (Array.isArray(body.maintainers)) { + maintainers = body.maintainers + .filter((maintainer): maintainer is Human => isObject(maintainer) && typeof maintainer.name === "string") + .map(({ name, email }) => ({ name, email })); + if (maintainers.length === 0) throw new RegistryError(400, "Bad Request: a package needs one maintainer"); + } + + if (isObject(body.versions)) { + for (const key of Object.keys(document.versions)) { + if (Object.hasOwn(body.versions, key)) continue; + delete document.versions[key]; + stored.unpublished.add(key); + } + } + if (Object.keys(document.versions).length === 0) { + return this.#remove(stored); + } + + if (incomingTags !== null) { + const tags: Record = {}; + for (const [tag, target] of incomingTags) { + if (typeof target === "string" && own(document.versions, target)) tags[tag] = target; + } + document["dist-tags"] = tags; + } else { + for (const [tag, target] of Object.entries(document["dist-tags"])) { + if (!own(document.versions, target)) delete document["dist-tags"][tag]; + } + } + // Every package has a `latest`. When its version goes away, the highest version that is left takes the tag. + document["dist-tags"].latest ??= highestVersion(Object.keys(document.versions))!; + + if (maintainers !== null) document.maintainers = maintainers; + return this.#commit(stored, new Date()); + }); + } + + /** `DELETE //-rev/`: `npm unpublish --force`. */ + unpublish(name: PackageName, revision: string, user: User): Promise { + return this.#exclusive(name.name, async () => { + const stored = await this.#forWrite(name, user); + checkRevision(stored, revision); + return this.#remove(stored); + }); + } + + #remove(stored: StoredPackage): PublishResult { + const versions = new Set([...stored.unpublished, ...Object.keys(stored.document.versions)]); + this.#loaded.set(stored.name.name, Promise.resolve(null)); + this.#removed.set(stored.name.name, { at: Date.now(), versions }); + return { ok: true, id: stored.name.name, rev: stored.document._rev ?? "" }; + } + + /** `DELETE //-//-rev/`: the last step of `npm unpublish `. */ + removeTarball(name: PackageName, filename: string, revision: string, user: User): Promise { + return this.#exclusive(name.name, async () => { + const stored = await this.#forWrite(name, user); + checkRevision(stored, revision); + for (const version of Object.values(stored.document.versions)) { + if (tarballFilename(version, name.basename) === filename) { + throw new RegistryError(400, `Bad Request: ${filename} belongs to version ${version.version}`); + } + } + if (!stored.tarballs.delete(filename)) throw notFound(); + stored.tarballHashes.delete(filename); + return { ok: true, id: name.name, rev: stored.document._rev ?? "" }; + }); + } + + setTag(name: PackageName, tag: string, version: unknown, user: User): Promise { + return this.#exclusive(name.name, async () => { + const stored = await this.#forWrite(name, user); + if (!isValidTag(tag)) throw new RegistryError(400, `Bad Request: "${tag}" is not a valid dist-tag`); + if (typeof version !== "string" || !own(stored.document.versions, version)) { + throw new RegistryError(404, `version not found: ${version}`); + } + stored.document["dist-tags"][tag] = version; + return this.#commit(stored, new Date()); + }); + } + + removeTag(name: PackageName, tag: string, user: User): Promise { + return this.#exclusive(name.name, async () => { + const stored = await this.#forWrite(name, user); + if (tag === "latest") throw new RegistryError(400, `Bad Request: the "latest" tag cannot be removed`); + if (!Object.hasOwn(stored.document["dist-tags"], tag)) throw new RegistryError(404, `dist-tag not found: ${tag}`); + delete stored.document["dist-tags"][tag]; + return this.#commit(stored, new Date()); + }); + } + + setAccess(name: PackageName, access: unknown, user: User): Promise { + return this.#exclusive(name.name, async () => { + const stored = await this.#forWrite(name, user); + if (access !== "public" && access !== "restricted") { + throw new RegistryError(400, `Bad Request: access must be "public" or "restricted"`); + } + if (access === "restricted" && name.scope === null) { + throw new RegistryError(400, "Bad Request: only a scoped package can be restricted"); + } + stored.access = access; + }); + } + + /** The md5 of a tarball, which is its entity tag. */ + async tarballHash(stored: StoredPackage, filename: string, tarball: Blob): Promise { + let hash = stored.tarballHashes.get(filename); + if (hash === undefined) { + hash = new Bun.CryptoHasher("md5").update(await tarball.bytes()).digest("hex"); + stored.tarballHashes.set(filename, hash); + } + return hash; + } +} + +/** The names in a directory, or nothing when it is not a directory. Each other error is an error. */ +async function entries(directory: string): Promise { + try { + return await readdir(directory); + } catch (error) { + const code = error instanceof Error && "code" in error ? error.code : undefined; + if (code === "ENOENT" || code === "ENOTDIR") return []; + throw error; + } +} + +function isMaintainer(stored: StoredPackage | null, user: string): boolean { + return stored?.document.maintainers?.some(maintainer => maintainer.name === user) ?? false; +} + +function forbidden(name: string) { + return new RegistryError( + 403, + `You do not have permission to publish "${name}". Are you logged in as the correct user?`, + ); +} + +function checkRevision(stored: StoredPackage, revision: string) { + // A document of the storage directory can have an empty `_rev`. Then there is nothing to compare. + const current = stored.document._rev; + if (current && revision !== current) throw new RegistryError(409, "Document update conflict."); +} + +function readAttachment(attachments: unknown): Buffer { + const tarballs = isObject(attachments) ? Object.entries(attachments).filter(([key]) => key.endsWith(".tgz")) : []; + if (tarballs.length !== 1) throw new RegistryError(400, "Bad Request: a publish holds one tarball"); + const [key, attachment] = tarballs[0]; + if (!isObject(attachment) || typeof attachment.data !== "string") { + throw new RegistryError(400, `Bad Request: _attachments["${key}"].data must be base64`); + } + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(attachment.data) || attachment.data.length % 4 !== 0) { + throw new RegistryError(400, `Bad Request: _attachments["${key}"].data must be base64`); + } + const tarball = Buffer.from(attachment.data, "base64"); + if (tarball.byteLength === 0) throw new RegistryError(400, "Bad Request: the tarball is empty"); + if (typeof attachment.length === "number" && attachment.length !== tarball.byteLength) { + throw new RegistryError( + 400, + `Bad Request: _attachments["${key}"].length is ${attachment.length}, the tarball has ${tarball.byteLength} bytes`, + ); + } + return tarball; +} + +function verifyIntegrity(integrity: string, tarball: Buffer) { + for (const entry of integrity.trim().split(/\s+/)) { + const dash = entry.indexOf("-"); + const algorithm = integrityAlgorithms.find(candidate => candidate === entry.slice(0, dash)); + if (algorithm === undefined) continue; + const expected = entry.slice(dash + 1).split("?", 1)[0]; + const actual = new Bun.CryptoHasher(algorithm).update(tarball).digest("base64"); + if (expected !== actual) { + throw new RegistryError(400, `Bad Request: dist.integrity is ${entry}, the tarball has ${algorithm}-${actual}`); + } + } +} diff --git a/test/packages/registry/src/packument.ts b/test/packages/registry/src/packument.ts new file mode 100644 index 000000000000..17b938711eea --- /dev/null +++ b/test/packages/registry/src/packument.ts @@ -0,0 +1,163 @@ +/** + * The package metadata document ("packument") and its two wire forms. + * https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md + */ + +export interface Human { + name?: string; + email?: string; + url?: string; +} + +export interface Dist { + tarball: string; + shasum?: string; + integrity?: string; + fileCount?: number; + unpackedSize?: number; + [key: string]: unknown; +} + +export interface VersionDocument { + name: string; + version: string; + dist: Dist; + deprecated?: string; + scripts?: Record; + [key: string]: unknown; +} + +export interface Packument { + _id?: string; + _rev?: string; + name: string; + "dist-tags": Record; + versions: Record; + time?: Record; + maintainers?: Human[]; + users?: Record; + readme?: string; + readmeFilename?: string; + [key: string]: unknown; +} + +export const abbreviatedContentType = "application/vnd.npm.install-v1+json"; + +/** + * The value under a key that a client chose. A name such as `constructor` is a key of every object through the + * prototype, and it is not a version or a tag. + */ +export function own(record: Record, key: string): T | undefined { + return Object.hasOwn(record, key) ? record[key] : undefined; +} + +/** + * The registry answers with the abbreviated document when the Accept header holds this media type anywhere, in + * this exact case. It does not weigh q values: `application/json; q=1.0, application/vnd.npm.install-v1+json; q=0.1` + * gets the abbreviated document too. + */ +export function wantsAbbreviated(accept: string | null): boolean { + return accept !== null && accept.includes(abbreviatedContentType); +} + +/** The allow list of the abbreviated version object, in the order of the registry documentation. */ +const abbreviatedVersionFields = [ + "name", + "version", + "deprecated", + "dependencies", + "acceptDependencies", + "optionalDependencies", + "devDependencies", + "bundleDependencies", + "peerDependencies", + "peerDependenciesMeta", + "bin", + "directories", + "dist", + "engines", + "funding", + "cpu", + "os", +] as const; + +/** Fields that only the storage layer of verdaccio writes. They are not part of a packument on the wire. */ +const storageOnlyFields = new Set(["_attachments", "_distfiles", "_uplinks"]); + +export function hasInstallScript(version: VersionDocument): boolean { + if (version.hasInstallScript === true) return true; + const scripts = version.scripts; + if (scripts === null || typeof scripts !== "object") return false; + return Boolean(scripts.preinstall || scripts.install || scripts.postinstall); +} + +/** `pkg-1.0.0.tgz` of `http://host/@scope/pkg/-/@scope/pkg-1.0.0.tgz?x=1`. */ +export function tarballFilename(version: VersionDocument, fallbackBasename: string): string { + const tarball = version.dist?.tarball; + if (typeof tarball === "string" && tarball.length > 0) { + const end = tarball.search(/[?#]/); + const path = end === -1 ? tarball : tarball.slice(0, end); + const filename = path.slice(path.lastIndexOf("/") + 1); + if (filename.length > 0) return filename; + } + return `${fallbackBasename}-${version.version}.tgz`; +} + +export interface RenderContext { + /** The canonical name, which can differ from `name` in a stored document. */ + name: string; + basename: string; + /** Origin plus path prefix of the registry as the client addressed it, no trailing slash. */ + base: string; + /** Used for `modified` when the document has no `time.modified`. */ + modified: Date; +} + +function tarballUrl(context: RenderContext, version: VersionDocument): string { + return `${context.base}/${context.name}/-/${tarballFilename(version, context.basename)}`; +} + +export function renderVersion(context: RenderContext, version: VersionDocument): VersionDocument { + return { ...version, dist: { ...version.dist, tarball: tarballUrl(context, version) } }; +} + +export function renderFull(context: RenderContext, document: Packument): Packument { + const rendered: Record = {}; + for (const key in document) { + if (storageOnlyFields.has(key)) continue; + rendered[key] = document[key]; + } + const versions: Record = {}; + for (const key in document.versions) { + versions[key] = renderVersion(context, document.versions[key]); + } + rendered.versions = versions; + return rendered as Packument; +} + +export function renderAbbreviated(context: RenderContext, document: Packument) { + const versions: Record> = {}; + for (const key in document.versions) { + const version = document.versions[key]; + const abbreviated: Record = {}; + for (const field of abbreviatedVersionFields) { + if (field === "dist") { + abbreviated.dist = { ...version.dist, tarball: tarballUrl(context, version) }; + } else if (field === "bundleDependencies") { + const bundled = version.bundleDependencies ?? version.bundledDependencies; + if (bundled !== undefined) abbreviated.bundleDependencies = bundled; + } else if (version[field] !== undefined) { + abbreviated[field] = version[field]; + } + } + if (version._hasShrinkwrap === true) abbreviated._hasShrinkwrap = true; + if (hasInstallScript(version)) abbreviated.hasInstallScript = true; + versions[key] = abbreviated; + } + return { + name: document.name, + "dist-tags": document["dist-tags"], + versions, + modified: document.time?.modified ?? context.modified.toISOString(), + }; +} diff --git a/test/packages/registry/src/registry.ts b/test/packages/registry/src/registry.ts new file mode 100644 index 000000000000..7e06b8aa0004 --- /dev/null +++ b/test/packages/registry/src/registry.ts @@ -0,0 +1,849 @@ +import type { BunFile, Server, TLSOptions } from "bun"; +import { Advisories } from "./advisories.ts"; +import { Auth, type Credentials, type Token, type User } from "./auth.ts"; +import { + RegistryError, + bodyOf, + lastModified, + matchesEntityTag, + methodNotAllowed, + notFound, + parseRange, + rangeNotSatisfiable, + readJson, + resourceNotFound, + send, + sendError, + sendJson, +} from "./http.ts"; +import { parsePackageName, type PackageName } from "./names.ts"; +import { Packages, type AccessRules, type StoredPackage } from "./packages.ts"; +import { + abbreviatedContentType, + own, + renderAbbreviated, + renderFull, + renderVersion, + tarballFilename, + wantsAbbreviated, + type RenderContext, +} from "./packument.ts"; + +export interface RecordedRequest { + method: string; + /** Path and query, as the client sent them. */ + path: string; + headers: Record; + status: number; +} + +export interface RegistryOptions { + /** + * A directory with one folder per package, `/package.json` (the packument) next to the tarballs. This is + * the storage layout of verdaccio. The registry reads it and never writes it: what a client publishes stays in + * memory. + */ + storage?: string; + /** Who reads and writes which package, by name pattern. See `AccessRules`. */ + access?: AccessRules; + /** The address to listen on. The default is the IPv4 loopback. */ + hostname?: string; + /** The default, 0, takes a free port. */ + port?: number; + tls?: TLSOptions; + /** + * The base of every tarball URL, for example `https://registry.example.com`. The default is the origin the + * client used, so that the registry also works through a proxy or by another host name. + */ + publicUrl?: string; + /** Sent as the `npm-notice` header on the answers to an authenticated client. */ + notice?: string; + /** Keep every request and the status of its answer in `requests`. */ + recordRequests?: boolean; + /** + * Runs before the registry handles a request. A returned Response is the answer. This is how a test makes the + * registry fail or stall for one URL. It gets a copy of the request, so it can read the body of a request that + * the registry answers. + */ + intercept?: ( + request: Request, + registry: Registry, + ) => Response | void | undefined | Promise; +} + +const otpMessage = "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA."; +const packumentCacheControl = "public, max-age=300"; +const tarballCacheControl = "public, immutable, max-age=31557600"; +const couchUserPrefix = "org.couchdb.user:"; + +function decodeSegment(segment: string): string | null { + try { + return decodeURIComponent(segment); + } catch { + return null; + } +} + +function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +/** Takes a package name from the front of a path: `@scope%2fname`, `@scope/name` or `name`. */ +function takePackageName(segments: string[]): { name: PackageName; rest: string[] } | null { + const [first, second] = segments; + if (first === undefined) return null; + let name = first; + let rest = segments.slice(1); + if (first.startsWith("@") && !first.includes("/")) { + if (second === undefined) return null; + name = `${first}/${second}`; + rest = segments.slice(2); + } + const parsed = parsePackageName(name); + return parsed && { name: parsed, rest }; +} + +/** + * The host of the URL of a registry that listens on `hostname`. `localhost` reaches the IPv4 loopback, which is + * the default, and a registry that listens on every address. It does not reach another address: bun connects to + * 127.0.0.1 for `localhost`. + */ +export function urlHost(hostname: string | undefined): string { + if (hostname === undefined) return "localhost"; + if (["localhost", "127.0.0.1", "0.0.0.0", "::"].includes(hostname)) return "localhost"; + // An IPv6 address has brackets in a URL. + return hostname.includes(":") ? `[${hostname}]` : hostname; +} + +function validDate(value: unknown, fallback: Date): Date { + const date = typeof value === "string" ? new Date(value) : fallback; + return Number.isNaN(date.getTime()) ? fallback : date; +} + +/** + * An npm registry for tests. + * + * What a package manager reads follows registry.npmjs.org as it answered in September 2026: the packument in both + * forms, a version, a tarball, the dist-tags, `whoami`. What it writes follows the npm documentation and the npm + * client, because nothing was written to the public registry to compare. The known differences are in the + * description of the pull request that added this package. + * + * ```ts + * using registry = new Registry({ storage: "./packages" }).start(); + * const { token } = registry.auth.createToken(registry.auth.addUser("alice", "secret")); + * await Bun.$`bun publish --registry ${registry.url}`.env({ ...process.env, NPM_CONFIG_TOKEN: token }); + * ``` + */ +export class Registry { + readonly auth = new Auth(); + readonly advisories = new Advisories(); + readonly packages: Packages; + /** Filled when `recordRequests` is set. */ + readonly requests: RecordedRequest[] = []; + readonly options: Readonly; + #server: Server | null = null; + /** The first error that is not an answer of the registry, for example a failed `expect()` in `intercept`. */ + #failure: { error: unknown } | null = null; + + constructor(options: RegistryOptions = {}) { + this.options = options; + this.packages = new Packages(options.storage, options.access); + } + + /** Opens the port. A second call does nothing. */ + start(): this { + this.#server ??= Bun.serve({ + hostname: this.options.hostname ?? "127.0.0.1", + port: this.options.port ?? 0, + tls: this.options.tls, + // The default of 128 MB is the size of the JSON body. A tarball in it is base64. + maxRequestBodySize: 512 * 1024 * 1024, + // A client under load can leave a connection alone for longer than the default of 10 seconds, and then use + // it again at the moment the server closes it. + idleTimeout: 0, + development: false, + // `development: false` turns SO_REUSEPORT on. A second registry on the same port has other users and other + // packages, and the kernel would hand each connection to one of the two. + reusePort: false, + fetch: request => this.fetch(request), + }); + return this; + } + + /** + * Closes the port and every open connection. Users, tokens and packages stay, so `start` can follow. + * + * Throws the first error that `intercept` or the registry itself threw while it answered a request. The client + * got a 500 for it, which a test can miss. This makes the test fail. + */ + stop() { + this.#server?.stop(true); + this.#server = null; + const failure = this.#failure; + this.#failure = null; + if (failure !== null) throw failure.error; + } + + [Symbol.dispose]() { + this.stop(); + } + + get listening(): boolean { + return this.#server !== null; + } + + get port(): number { + const port = this.#server?.port; + if (port === undefined) throw new Error("The registry has no port before start()"); + return port; + } + + /** + * The value for `registry=` in an `.npmrc` or a `bunfig.toml`. It is `http://localhost:/` for a registry + * that listens on the loopback or on every address, and names the `hostname` of the options for each other one. + */ + get url(): string { + return `${this.options.tls ? "https" : "http"}://${urlHost(this.options.hostname)}:${this.port}/`; + } + + /** Answers one request. `start` passes every request of the port to it. */ + async fetch(request: Request): Promise { + let response: Response; + try { + const intercepted = await this.options.intercept?.(request.clone(), this); + response = intercepted instanceof Response ? intercepted : await this.#route(request, new URL(request.url)); + } catch (error) { + if (error instanceof RegistryError) { + response = sendError(request, error); + } else { + // Not printed here: `stop` throws it, with the stack. + this.#failure ??= { error }; + response = sendJson(request, { error: "Internal Server Error" }, { status: 500 }); + } + } + if (this.options.recordRequests) { + const url = new URL(request.url); + this.requests.push({ + method: request.method, + path: url.pathname + url.search, + headers: Object.fromEntries(request.headers), + status: response.status, + }); + } + return response; + } + + #route(request: Request, url: URL): Promise | Response { + const segments: string[] = []; + for (const raw of url.pathname.split("/")) { + if (raw.length === 0) continue; + const segment = decodeSegment(raw); + if (segment === null) throw notFound(); + segments.push(segment); + } + if (segments.length === 0) { + allow(request, "GET", "HEAD"); + return sendJson(request, {}, { headers: { "cache-control": tarballCacheControl } }); + } + if (segments[0] === "-") return this.#service(request, url, segments.slice(1)); + if (segments.length === 3 && segments[1] === "cli" && (segments[0] === "login" || segments[0] === "auth")) { + return this.#browser(request, segments[2]); + } + return this.#package(request, url, segments); + } + + /** + * Stands in for the page of the npm website that the user opens to log in or to approve a write: + * `/login/cli/` and `/auth/cli/`. A request with the credentials of a user approves the session. + */ + #browser(request: Request, id: string): Response { + allow(request, "GET", "POST"); + const credentials = this.#user(request); + const session = this.auth.sessions.get(id); + if (session === undefined || (session.user !== null && session.user !== credentials.user.name)) throw notFound(); + this.auth.approveSession(session.id, credentials.user.name); + return sendJson(request, { ok: true }); + } + + #base(url: URL): string { + return (this.options.publicUrl ?? url.origin).replace(/\/+$/, ""); + } + + #context(url: URL, stored: StoredPackage): RenderContext { + return { + name: stored.name.name, + basename: stored.name.basename, + base: this.#base(url), + modified: stored.modified, + }; + } + + // Credentials + + #noticed(credentials: Credentials, headers: Record = {}): Record { + if (this.options.notice !== undefined && credentials.kind === "user") headers["npm-notice"] = this.options.notice; + return headers; + } + + /** The user of a request that only a logged in client can make. */ + #user(request: Request, message = "Unauthorized"): Extract { + const credentials = this.auth.credentials(request); + if (credentials.kind === "user") return credentials; + // The registry tells a client without credentials what is missing. It tells a client with wrong ones nothing. + throw new RegistryError(401, message, credentials.kind === "invalid" ? { body: {} } : {}); + } + + /** The user of a request to the account endpoints. Their 401 has no body. */ + #account(request: Request): Extract { + const credentials = this.auth.credentials(request); + if (credentials.kind === "user") return credentials; + throw new RegistryError(401, "Unauthorized", { + body: undefined, + headers: credentials.kind === "invalid" ? { "www-authenticate": "Basic, Bearer" } : {}, + }); + } + + /** The user of a request that changes something. It needs a token that can write, and a one-time password. */ + #writer(request: Request, url: URL, message = "You must be logged in to publish packages."): User { + const { user, token } = this.#user(request, message); + if (token?.readonly) { + throw new RegistryError(403, "This token is read-only. Use a token that can publish."); + } + if (user.tfa === "auth-and-writes" && !token?.automation) this.#otp(request, url, user); + return user; + } + + #otp(request: Request, url: URL, user: User) { + if (this.auth.isValidOtp(user, request.headers.get("npm-otp"))) return; + const body: Record = { error: otpMessage }; + if (request.headers.get("npm-auth-type") === "web") { + // The client opens `authUrl` in a browser and waits at `doneUrl` for the code. + const session = this.auth.openSession("otp", user.name); + body.authUrl = `${this.#base(url)}/auth/cli/${session.id}`; + body.doneUrl = `${this.#base(url)}/-/v1/done?authId=${session.id}`; + } + throw new RegistryError(401, otpMessage, { body, headers: { "www-authenticate": "OTP" } }); + } + + // Packages + + async #package(request: Request, url: URL, segments: string[]): Promise { + const taken = takePackageName(segments); + if (taken === null) throw notFound(); + const { name, rest } = taken; + + if (rest.length === 0) { + if (request.method === "PUT") return this.#write(request, url, name); + allow(request, "GET", "HEAD", "PUT"); + const credentials = this.auth.credentials(request); + return this.#packument(request, url, await this.packages.read(name.name, credentials)); + } + + if (rest[0] === "-rev" && rest.length === 2) { + allow(request, "PUT", "DELETE"); + const user = this.#writer(request, url); + const result = + request.method === "PUT" + ? await this.packages.replace(name, rest[1], await readJson(request), user) + : await this.packages.unpublish(name, rest[1], user); + return sendJson(request, result); + } + + if (rest[0] === "-") { + if (rest.length === 2) { + allow(request, "GET", "HEAD"); + const stored = await this.packages.read(name.name, this.auth.credentials(request)); + return this.#tarball(request, stored, rest[1]); + } + if (rest.length === 4 && rest[2] === "-rev") { + allow(request, "DELETE"); + const user = this.#writer(request, url); + return sendJson(request, await this.packages.removeTarball(name, rest[1], rest[3], user)); + } + throw resourceNotFound(url.pathname); + } + + if (rest.length === 1) { + allow(request, "GET", "HEAD"); + const stored = await this.packages.read(name.name, this.auth.credentials(request)); + const wanted = rest[0]; + const { versions, "dist-tags": tags } = stored.document; + const version = own(versions, wanted) ?? own(versions, own(tags, wanted) ?? ""); + if (version === undefined) { + throw new RegistryError(404, `version not found: ${wanted}`, { body: `version not found: ${wanted}` }); + } + return sendJson(request, renderVersion(this.#context(url, stored), version), { + headers: { "cache-control": "max-age=300", "vary": "accept-encoding, accept" }, + }); + } + + throw resourceNotFound(url.pathname); + } + + #packument(request: Request, url: URL, stored: StoredPackage): Response { + // A client asks with `?write=true` for the document that it edits and sends back. The answer is the full + // document, whatever the Accept header says, and it has no validators. + const forWrite = url.searchParams.get("write") === "true"; + const abbreviated = !forWrite && wantsAbbreviated(request.headers.get("accept")); + const context = this.#context(url, stored); + const key = `${abbreviated ? "abbreviated" : "full"} ${context.base}`; + let body = stored.rendered.get(key); + if (body === undefined) { + body = bodyOf(abbreviated ? renderAbbreviated(context, stored.document) : renderFull(context, stored.document)); + stored.rendered.set(key, body); + } + if (forWrite) { + return send(request, body, { + headers: { "cache-control": packumentCacheControl, "vary": "accept-encoding, accept" }, + }); + } + return send(request, body, { + conditional: true, + headers: { + "content-type": abbreviated ? abbreviatedContentType : "application/json", + "cache-control": packumentCacheControl, + "last-modified": lastModified(validDate(stored.document.time?.modified, stored.modified)), + "vary": "accept-encoding, accept", + }, + }); + } + + async #tarball(request: Request, stored: StoredPackage, filename: string): Promise { + const tarball = stored.tarballs.get(filename); + if (tarball === undefined) throw notFound(); + if ("exists" in tarball && !(await (tarball as BunFile).exists())) throw notFound(); + + const md5 = await this.packages.tarballHash(stored, filename, tarball); + const version = Object.values(stored.document.versions).find( + candidate => tarballFilename(candidate, stored.name.basename) === filename, + ); + const published = validDate(version && stored.document.time?.[version.version], stored.modified); + const headers = new Headers({ + "etag": `"${md5}"`, + "last-modified": lastModified(published), + "cache-control": tarballCacheControl, + }); + + if (matchesEntityTag(request.headers.get("if-none-match"), md5)) { + headers.set("cache-control", packumentCacheControl); + return new Response(null, { status: 304, headers }); + } + + const range = parseRange(request.headers.get("range"), tarball.size); + if (range === "unsatisfiable") { + headers.set("content-range", `bytes */${tarball.size}`); + headers.delete("cache-control"); + return sendJson(request, rangeNotSatisfiable, { status: 416, headers: toObject(headers) }); + } + + headers.set("content-type", "application/octet-stream"); + // The bytes, not the file: a file body makes Bun.serve add a Content-Disposition header that the registry does + // not send. + let body = await tarball.bytes(); + if (range === null) { + headers.set("accept-ranges", "bytes"); + } else { + headers.set("content-range", `bytes ${range.start}-${range.end}/${tarball.size}`); + body = body.subarray(range.start, range.end + 1); + } + if (request.method === "HEAD") { + headers.set("content-length", String(body.byteLength)); + return new Response(null, { status: range === null ? 200 : 206, headers }); + } + return new Response(body, { status: range === null ? 200 : 206, headers }); + } + + async #write(request: Request, url: URL, name: PackageName): Promise { + const credentials = this.#user(request, "You must be logged in to publish packages."); + const body = await readJson(request); + const attachments = isObject(body) && isObject(body._attachments) ? Object.keys(body._attachments) : []; + const starsOnly = isObject(body) && isObject(body.users) && body.versions === undefined; + + // Everyone who is logged in can star a package. Each other change needs the right to publish. + const user = starsOnly ? credentials.user : this.#writer(request, url); + const result = + attachments.length > 0 + ? await this.packages.publish(name, body, user) + : await this.packages.change(name, body, user); + return sendJson(request, result, { headers: this.#noticed(credentials) }); + } + + // Services under /-/ + + async #service(request: Request, url: URL, segments: string[]): Promise { + const path = segments.join("/"); + + switch (path) { + case "ping": { + allow(request, "GET", "HEAD"); + return sendJson(request, {}); + } + case "whoami": { + allow(request, "GET", "HEAD"); + const credentials = this.#user(request); + return sendJson(request, { username: credentials.user.name }, { headers: this.#noticed(credentials) }); + } + case "v1/login": { + allow(request, "POST"); + await readJson(request); + const session = this.auth.openSession("login", null); + const base = this.#base(url); + return sendJson(request, { + loginUrl: `${base}/login/cli/${session.id}`, + doneUrl: `${base}/-/v1/done?sessionId=${session.id}`, + }); + } + case "v1/done": { + allow(request, "GET"); + return this.#done(request, url); + } + case "v1/search": { + allow(request, "GET", "HEAD"); + return this.#search(request, url); + } + case "npm/v1/user": { + allow(request, "GET", "HEAD", "POST"); + return this.#profile(request, url); + } + case "npm/v1/tokens": { + allow(request, "GET", "HEAD", "POST"); + return this.#tokens(request, url); + } + case "npm/v1/keys": { + allow(request, "GET", "HEAD"); + // This registry does not sign what it stores, so it has no keys to publish. + return sendJson(request, { keys: [] }); + } + case "npm/v1/security/advisories/bulk": { + allow(request, "POST"); + const body = await readJson(request); + if (!isObject(body)) throw new RegistryError(400, "Bad Request: the body must be a JSON object"); + return sendJson(request, this.advisories.lookup(body)); + } + } + + if (segments[0] === "user" && segments[1]?.startsWith(couchUserPrefix)) { + const name = segments[1].slice(couchUserPrefix.length); + const revised = segments.length === 4 && segments[2] === "-rev"; + if (segments.length === 2 || revised) { + if (request.method === "PUT") return this.#login(request, url, name); + if (!revised) { + allow(request, "GET", "HEAD", "PUT"); + return this.#couchUser(request, name); + } + } + } + + if (path.startsWith("user/token/") && segments.length === 3) { + allow(request, "DELETE"); + const credentials = this.#user(request); + if (!this.auth.revokeToken(segments[2], credentials.user.name)) throw notFound(); + return sendJson(request, { ok: true }); + } + + if (path.startsWith("npm/v1/tokens/token/") && segments.length === 5) { + allow(request, "DELETE"); + const credentials = this.#user(request); + if (!this.auth.revokeToken(segments[4], credentials.user.name)) throw notFound(); + return new Response(null, { status: 204 }); + } + + if (segments[0] === "package") { + const taken = takePackageName(segments.slice(1)); + if (taken !== null) return this.#packageService(request, url, taken.name, taken.rest); + } + + throw resourceNotFound(url.pathname); + } + + async #packageService(request: Request, url: URL, name: PackageName, rest: string[]): Promise { + const credentials = this.auth.credentials(request); + const readable = async () => { + const stored = await this.packages.get(name.name); + return stored !== null && this.packages.canRead(stored, credentials) ? stored : null; + }; + + if (rest[0] === "dist-tags" && rest.length === 1) { + allow(request, "GET", "HEAD"); + const stored = await readable(); + if (stored === null) throw new RegistryError(404, "Not Found", { body: "Not Found" }); + return sendJson(request, stored.document["dist-tags"]); + } + if (rest[0] === "dist-tags" && rest.length === 2) { + allow(request, "PUT", "DELETE"); + // Only a change of `latest` counts as a write that needs a one-time password. + const user = rest[1] === "latest" ? this.#writer(request, url) : this.#writerWithoutOtp(request); + if (request.method === "DELETE") await this.packages.removeTag(name, rest[1], user); + else await this.packages.setTag(name, rest[1], await readJson(request), user); + return sendJson(request, { ok: "dist-tags updated" }); + } + if (rest[0] === "collaborators" && rest.length === 1) { + allow(request, "GET", "HEAD"); + // This endpoint looks at the credentials of a public package too. A packument does not. + if (credentials.kind === "invalid") { + throw new RegistryError(401, "You must be logged in to publish packages.", { + headers: { "www-authenticate": "Basic, Bearer" }, + }); + } + const stored = await readable(); + if (stored === null) throw new RegistryError(404, "Package not found"); + const maintainers = stored.document.maintainers ?? []; + return sendJson(request, Object.fromEntries(maintainers.map(maintainer => [maintainer.name, "write"]))); + } + if (rest[0] === "visibility" && rest.length === 1) { + allow(request, "GET", "HEAD"); + // A package that is not there is not public. The answer is not a 404. + return sendJson(request, { public: (await readable())?.access === "public" }); + } + if (rest[0] === "access" && rest.length === 1) { + allow(request, "POST"); + const user = this.#writer(request, url); + const body = await readJson(request); + await this.packages.setAccess(name, isObject(body) ? body.access : undefined, user); + return sendJson(request, {}); + } + throw resourceNotFound(url.pathname); + } + + #writerWithoutOtp(request: Request): User { + const { user, token } = this.#user(request); + if (token?.readonly) throw new RegistryError(403, "This token is read-only. Use a token that can publish."); + return user; + } + + /** `npm adduser` and `npm login` with `--auth-type=legacy`. */ + async #login(request: Request, url: URL, name: string): Promise { + const body = await readJson(request); + if (!isObject(body) || typeof body.name !== "string" || typeof body.password !== "string") { + throw new RegistryError(400, "Bad Request: name and password are required", { body: { ok: false } }); + } + if (body.name !== name) { + throw new RegistryError(400, "Bad Request: the name of the body is not the name of the URL"); + } + + let user = this.auth.users.get(name); + if (user === undefined) { + // `npm login` sends no email. A user that does not exist cannot log in. + if (typeof body.email !== "string") { + throw new RegistryError(400, `There is no user with the username "${name}".`); + } + user = this.auth.addUser(name, body.password, { email: body.email }); + } else { + if (!this.auth.verifyPassword(user, body.password)) { + throw new RegistryError(401, "Unauthorized", { body: { ok: false } }); + } + if (user.tfa !== null) this.#otp(request, url, user); + } + + const { token } = this.auth.createToken(user); + return sendJson( + request, + { ok: true, id: `${couchUserPrefix}${name}`, rev: "_we_dont_use_revs_any_more", token }, + { status: 201, headers: { "cache-control": "no-cache, no-store" } }, + ); + } + + #couchUser(request: Request, name: string): Response { + let credentials: Extract; + try { + credentials = this.#user(request); + } catch { + throw new RegistryError(401, "Unauthorized", { body: { ok: false } }); + } + const user = this.auth.users.get(name); + if (user === undefined || user.name !== credentials.user.name) throw notFound(); + return sendJson(request, { + _id: `${couchUserPrefix}${user.name}`, + name: user.name, + email: user.email, + type: "user", + roles: [], + date: user.created.toISOString(), + }); + } + + /** Where a client waits for the user to finish in the browser. */ + #done(request: Request, url: URL): Response { + const id = url.searchParams.get("sessionId") ?? url.searchParams.get("authId"); + const session = id === null ? undefined : this.auth.sessions.get(id); + if (session === undefined) throw new RegistryError(404, "not found", { body: { message: "not found" } }); + session.polls++; + if (session.result === null) { + return sendJson(request, {}, { status: 202, headers: { "retry-after": "1" } }); + } + // A login token is handed out once. A one-time password stays until a write uses it. + if (session.kind === "login") this.auth.sessions.delete(session.id); + return sendJson(request, { token: session.result }); + } + + async #profile(request: Request, url: URL): Promise { + const credentials = this.#account(request); + const user = credentials.user; + if (request.method === "POST") { + const body = await readJson(request); + if (!isObject(body)) throw new RegistryError(400, "Bad Request: the body must be a JSON object"); + if (credentials.token?.readonly) throw new RegistryError(403, "This token is read-only."); + if (isObject(body.password)) { + const { old, new: next } = body.password; + if (typeof old !== "string" || typeof next !== "string" || !this.auth.verifyPassword(user, old)) { + throw new RegistryError(401, "The old password is not correct"); + } + if (user.tfa !== null) this.#otp(request, url, user); + this.auth.setPassword(user, next); + } + if (typeof body.email === "string") user.email = body.email; + if (typeof body.fullname === "string") user.fullname = body.fullname; + user.updated = new Date(); + } + return sendJson( + request, + { + tfa: user.tfa === null ? false : { pending: false, mode: user.tfa }, + name: user.name, + email: user.email, + email_verified: true, + created: user.created.toISOString(), + updated: user.updated.toISOString(), + cidr_whitelist: null, + fullname: user.fullname, + }, + { headers: this.#noticed(credentials) }, + ); + } + + async #tokens(request: Request, url: URL): Promise { + const credentials = this.#account(request); + const user = credentials.user; + const describe = (token: Token, value: string) => ({ + token: value, + key: token.key, + cidr_whitelist: token.cidr_whitelist, + readonly: token.readonly, + automation: token.automation, + created: token.created.toISOString(), + updated: token.updated.toISOString(), + }); + + if (request.method === "POST") { + const body = await readJson(request); + if (!isObject(body) || typeof body.password !== "string" || !this.auth.verifyPassword(user, body.password)) { + throw new RegistryError(401, "The password is not correct"); + } + if (credentials.token?.readonly) throw new RegistryError(403, "This token is read-only."); + if (user.tfa !== null) this.#otp(request, url, user); + const cidr = Array.isArray(body.cidr_whitelist) ? body.cidr_whitelist.filter(x => typeof x === "string") : null; + const token = this.auth.createToken(user, { + readonly: body.readonly === true, + automation: body.automation === true, + cidr_whitelist: cidr, + }); + return sendJson(request, describe(token, token.token)); + } + + const all = this.auth.tokensOf(user.name); + const perPage = clamp(Number(url.searchParams.get("perPage") ?? 10), 1, 9999); + const page = Number(url.searchParams.get("page") ?? 0); + if (!Number.isInteger(page) || page < 0 || (page > 0 && page * perPage >= all.length)) { + throw new RegistryError(400, "Bad Request: no such page"); + } + const urls: Record = {}; + if ((page + 1) * perPage < all.length) { + urls.next = `${this.#base(url)}/-/npm/v1/tokens?page=${page + 1}&perPage=${perPage}`; + } + return sendJson(request, { + objects: all + .slice(page * perPage, (page + 1) * perPage) + .map(token => describe(token, `${token.token.slice(0, 6)}...${token.token.slice(-4)}`)), + total: all.length, + urls, + }); + } + + async #search(request: Request, url: URL): Promise { + const credentials = this.auth.credentials(request); + const words = (url.searchParams.get("text") ?? "").toLowerCase().split(/\s+/).filter(Boolean); + const size = clamp(Number(url.searchParams.get("size") ?? 20), 0, 250); + const from = Math.max(0, Number(url.searchParams.get("from") ?? 0) || 0); + + const objects: { searchScore: number; package: Record }[] = []; + for (const name of await this.packages.names()) { + const stored = await this.packages.get(name); + if (stored === null || !this.packages.canRead(stored, credentials)) continue; + const document = stored.document; + const latest = own(document.versions, document["dist-tags"].latest ?? ""); + if (latest === undefined) continue; + const keywords = Array.isArray(latest.keywords) ? latest.keywords.filter(word => typeof word === "string") : []; + const description = typeof latest.description === "string" ? latest.description : undefined; + + // A package is found when each word matches. The score only orders the packages that are found. + let matches = true; + let searchScore = 0; + for (const word of words) { + const qualifier = word.match(/^(scope|keywords|maintainer|author):(.*)$/); + if (qualifier) { + const [, kind, value] = qualifier; + const author = isObject(latest.author) ? latest.author.name : latest.author; + const hit = + kind === "scope" + ? stored.name.scope === value.replace(/^@/, "") + : kind === "keywords" + ? value.split(",").some(keyword => keywords.includes(keyword)) + : kind === "maintainer" + ? (document.maintainers ?? []).some(maintainer => maintainer.name === value) + : typeof author === "string" && author.toLowerCase().includes(value); + if (hit) searchScore += 1; + else matches = false; + } else if (name === word) searchScore += 1000; + else if (name.includes(word)) searchScore += 100; + else if (keywords.some(keyword => keyword.toLowerCase() === word)) searchScore += 10; + else if (description?.toLowerCase().includes(word)) searchScore += 1; + else matches = false; + } + if (!matches) continue; + + const maintainers = (document.maintainers ?? []).map(({ name, email }) => ({ username: name, email })); + objects.push({ + searchScore, + package: { + name, + ...(stored.name.scope === null ? { scope: "unscoped" } : { scope: stored.name.scope }), + version: latest.version, + description, + keywords, + date: document.time?.[latest.version] ?? stored.modified.toISOString(), + links: { npm: `${this.#base(url)}/${name}` }, + publisher: maintainers[0], + maintainers, + }, + }); + } + objects.sort( + (a, b) => b.searchScore - a.searchScore || String(a.package.name).localeCompare(String(b.package.name)), + ); + return sendJson( + request, + { + objects: objects.slice(from, from + size).map(({ searchScore, package: found }) => ({ + package: found, + score: { final: 1, detail: { quality: 1, popularity: 1, maintenance: 1 } }, + searchScore, + })), + total: objects.length, + time: new Date().toISOString(), + }, + { headers: { "cache-control": "max-age=300", "vary": "accept-encoding, accept" } }, + ); + } +} + +function allow(request: Request, ...methods: string[]) { + if (!methods.includes(request.method)) throw methodNotAllowed(request.method, methods); +} + +function clamp(value: number, minimum: number, maximum: number): number { + return Number.isFinite(value) ? Math.min(maximum, Math.max(minimum, Math.trunc(value))) : minimum; +} + +function toObject(headers: Headers): Record { + return Object.fromEntries(headers); +} diff --git a/test/packages/registry/test-registry.ts b/test/packages/registry/test-registry.ts new file mode 100644 index 000000000000..2569ed8cbea2 --- /dev/null +++ b/test/packages/registry/test-registry.ts @@ -0,0 +1,103 @@ +import { write } from "bun"; +import { tempDir, type DirectoryTree } from "harness"; +import { join } from "node:path"; +import { Registry, type RegistryOptions } from "./src/registry.ts"; + +export * from "./index.ts"; + +/** `Bun.TOML.stringify` has the return type of `JSON.stringify`. For an object the result is always a string. */ +export const toml = (value: object): string => Bun.TOML.stringify(value)!; + +export type BunfigOptions = { + saveTextLockfile?: boolean; + /** Leave the registry out, so that the install goes to the default one. */ + npm?: boolean; + linker?: "isolated" | "hoisted"; + globalStore?: boolean; + publicHoistPattern?: string | string[]; + hoistPattern?: string | string[]; + hoist?: boolean; +}; + +/** The packages that bun's install tests share: `test/cli/install/registry/packages`. */ +export const fixturePackages = join(import.meta.dir, "..", "..", "cli", "install", "registry", "packages"); + +/** + * A registry that serves the fixture packages, with the helpers that bun's install tests need. + * Every instance has its own users, tokens and published packages, so test files do not affect each other. + * + * ```ts + * const registry = new TestRegistry(); + * beforeAll(() => registry.start()); + * afterAll(() => registry.stop()); + * ``` + */ +export class TestRegistry extends Registry { + readonly packagesPath: string; + + constructor(options: RegistryOptions = {}) { + const storage = options.storage ?? fixturePackages; + super({ + ...options, + storage, + access: { "@needs-auth/*": { read: "authenticated" }, ...options.access }, + }); + this.packagesPath = storage; + } + + registryUrl() { + return this.url; + } + + /** Creates a user through the API, as `npm adduser` does. Returns a token of the user. */ + async generateUser(username: string, password: string): Promise { + const response = await fetch(`${this.url}-/user/org.couchdb.user:${encodeURIComponent(username)}`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: username, password, email: `${username}@example.com` }), + }); + const body = await response.text(); + if (!response.ok) throw new Error(`Failed to create user ${username}: ${response.status} ${body}`); + return JSON.parse(body).token; + } + + /** A bunfig.toml that points to this registry with the token of a new user. */ + async authBunfig(user: string) { + const token = await this.generateUser(user, user); + return toml({ + install: { + cache: false, + registry: { url: this.url, token }, + }, + }); + } + + async createTestDir( + options: { bunfigOpts?: BunfigOptions; files?: DirectoryTree | string } = { + bunfigOpts: { linker: "hoisted" }, + files: {}, + }, + ) { + const packageDir = String(tempDir("registry-test-", options.files ?? {})); + await this.writeBunfig(packageDir, options.bunfigOpts); + return { packageDir, packageJson: join(packageDir, "package.json") }; + } + + async writeBunfig(dir: string, options: BunfigOptions = {}) { + await write( + join(dir, "bunfig.toml"), + toml({ + install: { + cache: join(dir, ".bun-cache"), + saveTextLockfile: options.saveTextLockfile, + registry: options.npm ? undefined : this.url, + linker: options.linker, + globalStore: options.globalStore, + publicHoistPattern: options.publicHoistPattern, + hoistPattern: options.hoistPattern, + hoist: options.hoist, + }, + }), + ); + } +} diff --git a/test/packages/registry/test/auth.test.ts b/test/packages/registry/test/auth.test.ts new file mode 100644 index 000000000000..b53d73f0c007 --- /dev/null +++ b/test/packages/registry/test/auth.test.ts @@ -0,0 +1,386 @@ +import { describe, expect, test } from "bun:test"; +import { Registry } from "../index.ts"; +import { jsonHeaders, pack, publishBody, request } from "./helpers.ts"; + +const otpMessage = "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA."; + +function login(registry: Registry, name: string, body: Record, headers: Record = {}) { + return request(`${registry.url}-/user/org.couchdb.user:${name}`, { + method: "PUT", + headers: { ...jsonHeaders(), ...headers }, + body: JSON.stringify({ _id: `org.couchdb.user:${name}`, name, type: "user", roles: [], ...body }), + }); +} + +describe("adduser and login", () => { + test("a new user gets a token", async () => { + using registry = new Registry().start(); + const reply = await login(registry, "alice", { password: "secret", email: "alice@example.com" }); + expect(reply.status).toBe(201); + expect(reply.json).toEqual({ + ok: true, + id: "org.couchdb.user:alice", + rev: "_we_dont_use_revs_any_more", + token: expect.stringMatching(/^npm_[0-9A-Za-z]{36}$/), + }); + expect(reply.headers["cache-control"]).toBe("no-cache, no-store"); + + const whoami = await request(`${registry.url}-/whoami`, { headers: jsonHeaders(reply.json.token) }); + expect(whoami).toMatchObject({ status: 200, json: { username: "alice" } }); + }); + + test("a user that exists logs in with the password", async () => { + using registry = new Registry().start(); + const created = await login(registry, "bob", { password: "secret", email: "bob@example.com" }); + // `npm login` sends no email. + const again = await login(registry, "bob", { password: "secret" }); + expect(again.status).toBe(201); + expect(again.json.token).not.toBe(created.json.token); + for (const { json } of [created, again]) { + const whoami = await request(`${registry.url}-/whoami`, { headers: jsonHeaders(json.token) }); + expect(whoami.json).toEqual({ username: "bob" }); + } + }); + + test("failures", async () => { + using registry = new Registry().start(); + await login(registry, "carol", { password: "secret", email: "carol@example.com" }); + + const wrongPassword = await login(registry, "carol", { password: "wrong" }); + expect(wrongPassword).toMatchObject({ status: 401, json: { ok: false } }); + + const unknown = await login(registry, "nobody", { password: "secret" }); + expect(unknown).toMatchObject({ + status: 400, + json: { error: `There is no user with the username "nobody".` }, + }); + + const otherName = await request(`${registry.url}-/user/org.couchdb.user:dave`, { + method: "PUT", + headers: jsonHeaders(), + body: JSON.stringify({ name: "erin", password: "secret", email: "erin@example.com" }), + }); + expect(otherName.status).toBe(400); + + const noPassword = await login(registry, "frank", { email: "frank@example.com" }); + expect(noPassword.status).toBe(400); + + const notJson = await request(`${registry.url}-/user/org.couchdb.user:grace`, { + method: "PUT", + headers: { "content-type": "text/plain" }, + body: JSON.stringify({ name: "grace", password: "secret", email: "grace@example.com" }), + }); + expect(notJson.status).toBe(415); + + const broken = await request(`${registry.url}-/user/org.couchdb.user:grace`, { + method: "PUT", + headers: jsonHeaders(), + body: "{", + }); + expect(broken.status).toBe(400); + expect([...registry.auth.users.keys()]).toEqual(["carol"]); + }); + + test("logout removes the token", async () => { + using registry = new Registry().start(); + const { token } = registry.auth.createToken(registry.auth.addUser("heidi", "secret")); + const other = registry.auth.createToken(registry.auth.addUser("ivan", "secret")).token; + + // A user cannot remove the token of another user. + const foreign = await request(`${registry.url}-/user/token/${token}`, { + method: "DELETE", + headers: jsonHeaders(other), + }); + expect(foreign.status).toBe(404); + + const reply = await request(`${registry.url}-/user/token/${token}`, { + method: "DELETE", + headers: jsonHeaders(token), + }); + expect(reply).toMatchObject({ status: 200, json: { ok: true } }); + const whoami = await request(`${registry.url}-/whoami`, { headers: jsonHeaders(token) }); + expect(whoami.status).toBe(401); + }); +}); + +describe("whoami", () => { + test("tells a client without credentials from a client with wrong ones", async () => { + using registry = new Registry().start(); + registry.auth.addUser("judy", "secret"); + + const none = await request(`${registry.url}-/whoami`); + expect({ status: none.status, body: none.text }).toEqual({ status: 401, body: `{"error":"Unauthorized"}` }); + // The registry does not ask for a scheme. + expect(none.headers).not.toHaveProperty("www-authenticate"); + + for (const authorization of ["Bearer npm_000000000000000000000000000000000000", "Basic anVkeTp3cm9uZw==", "x"]) { + const reply = await request(`${registry.url}-/whoami`, { headers: { authorization } }); + expect({ status: reply.status, body: reply.text }).toEqual({ status: 401, body: "{}" }); + } + + const basic = await request(`${registry.url}-/whoami`, { + headers: { authorization: `Basic ${btoa("judy:secret")}` }, + }); + expect(basic).toMatchObject({ status: 200, json: { username: "judy" } }); + }); + + test("npm-notice goes to a client that is logged in", async () => { + using registry = new Registry({ notice: "Please rotate your token." }).start(); + const { token } = registry.auth.createToken(registry.auth.addUser("kim", "secret")); + const reply = await request(`${registry.url}-/whoami`, { headers: jsonHeaders(token) }); + expect(reply.headers["npm-notice"]).toBe("Please rotate your token."); + expect((await request(`${registry.url}-/ping`)).headers).not.toHaveProperty("npm-notice"); + }); +}); + +describe("tokens and profile", () => { + test("create, list and delete", async () => { + using registry = new Registry().start(); + const first = registry.auth.createToken(registry.auth.addUser("lee", "secret")); + const endpoint = `${registry.url}-/npm/v1/tokens`; + + const wrong = await request(endpoint, { + method: "POST", + headers: jsonHeaders(first.token), + body: JSON.stringify({ password: "wrong" }), + }); + expect(wrong.status).toBe(401); + + const created = await request(endpoint, { + method: "POST", + headers: jsonHeaders(first.token), + body: JSON.stringify({ password: "secret", readonly: true, cidr_whitelist: ["10.0.0.0/8"] }), + }); + expect(created.status).toBe(200); + expect(created.json).toEqual({ + token: expect.stringMatching(/^npm_[0-9A-Za-z]{36}$/), + key: new Bun.CryptoHasher("sha512").update(created.json.token).digest("hex"), + cidr_whitelist: ["10.0.0.0/8"], + readonly: true, + automation: false, + created: expect.any(String), + updated: expect.any(String), + }); + + const listed = await request(endpoint, { headers: jsonHeaders(first.token) }); + expect(listed.json.total).toBe(2); + expect(listed.json.urls).toEqual({}); + expect(listed.json.objects.map((token: any) => token.key)).toEqual([first.key, created.json.key]); + // The value of a token is shown once, when it is created. + expect(JSON.stringify(listed.json)).not.toContain(created.json.token); + + const page = await request(`${endpoint}?perPage=1`, { headers: jsonHeaders(first.token) }); + expect(page.json.objects).toHaveLength(1); + expect(page.json.urls).toEqual({ next: `${endpoint}?page=1&perPage=1` }); + expect((await request(`${endpoint}?page=9`, { headers: jsonHeaders(first.token) })).status).toBe(400); + + const removed = await request(`${endpoint}/token/${created.json.key}`, { + method: "DELETE", + headers: jsonHeaders(first.token), + }); + expect({ status: removed.status, body: removed.text }).toEqual({ status: 204, body: "" }); + expect((await request(`${registry.url}-/whoami`, { headers: jsonHeaders(created.json.token) })).status).toBe(401); + + // The account endpoints answer a client that they do not know without a body. + expect(await request(endpoint)).toMatchObject({ status: 401, text: "", headers: {} }); + expect(await request(endpoint, { headers: jsonHeaders(created.json.token) })).toMatchObject({ + status: 401, + text: "", + headers: { "www-authenticate": "Basic, Bearer" }, + }); + const direct = await registry.fetch(new Request(endpoint, { headers: jsonHeaders(created.json.token) })); + expect({ status: direct.status, headers: Object.fromEntries(direct.headers), text: await direct.text() }).toEqual({ + status: 401, + headers: { "www-authenticate": "Basic, Bearer" }, + text: "", + }); + }); + + test("a read-only token reads and does not write", async () => { + using registry = new Registry().start(); + const user = registry.auth.addUser("mallory", "secret"); + const { token } = registry.auth.createToken(user, { readonly: true }); + expect((await request(`${registry.url}-/whoami`, { headers: jsonHeaders(token) })).status).toBe(200); + + const manifest = { name: "read-only", version: "1.0.0" }; + const reply = await request(`${registry.url}read-only`, { + method: "PUT", + headers: jsonHeaders(token), + body: JSON.stringify(publishBody(manifest, await pack(manifest))), + }); + expect(reply.status).toBe(403); + expect(await registry.packages.has("read-only")).toBe(false); + }); + + test("profile", async () => { + using registry = new Registry().start(); + const user = registry.auth.addUser("nina", "secret", { email: "nina@example.com", tfa: "auth-only", otp: ["1"] }); + const { token } = registry.auth.createToken(user); + const endpoint = `${registry.url}-/npm/v1/user`; + + const profile = await request(endpoint, { headers: jsonHeaders(token) }); + expect(profile.json).toEqual({ + tfa: { pending: false, mode: "auth-only" }, + name: "nina", + email: "nina@example.com", + email_verified: true, + created: user.created.toISOString(), + updated: user.updated.toISOString(), + cidr_whitelist: null, + fullname: "", + }); + const anonymous = await request(endpoint); + expect({ status: anonymous.status, text: anonymous.text }).toEqual({ status: 401, text: "" }); + expect(anonymous.headers).not.toHaveProperty("www-authenticate"); + // What the registry itself answers, without the HTTP server between: nothing that describes a body. + const direct = await registry.fetch(new Request(endpoint)); + expect({ status: direct.status, headers: Object.fromEntries(direct.headers), text: await direct.text() }).toEqual({ + status: 401, + headers: {}, + text: "", + }); + + const renamed = await request(endpoint, { + method: "POST", + headers: jsonHeaders(token), + body: JSON.stringify({ fullname: "Nina N." }), + }); + expect(renamed.json.fullname).toBe("Nina N."); + + // A new password needs the old one, and the one-time password of a user with two-factor authentication. + const change = (headers: Record, old = "secret") => + request(endpoint, { + method: "POST", + headers: { ...jsonHeaders(token), ...headers }, + body: JSON.stringify({ password: { old, new: "changed" } }), + }); + expect((await change({}, "wrong")).status).toBe(401); + expect(await change({})).toMatchObject({ status: 401, headers: { "www-authenticate": "OTP" } }); + expect((await change({ "npm-otp": "1" })).status).toBe(200); + expect(registry.auth.verifyPassword(user, "changed")).toBe(true); + }); +}); + +describe("one-time passwords", () => { + async function setup() { + const manifest = { name: "guarded", version: "1.0.0" }; + const body = JSON.stringify(publishBody(manifest, await pack(manifest))); + const registry = new Registry().start(); + const user = registry.auth.addUser("olga", "secret", { tfa: "auth-and-writes", otp: ["123456"] }); + const { token } = registry.auth.createToken(user); + const publish = (headers: Record = {}, bearer = token) => + request(`${registry.url}guarded`, { method: "PUT", headers: { ...jsonHeaders(bearer), ...headers }, body }); + return { registry, user, token, publish }; + } + + test("a write needs the code", async () => { + const { registry, publish } = await setup(); + using _ = registry; + + const withoutTheCode: Record[] = [{}, { "npm-otp": "000000" }]; + for (const headers of withoutTheCode) { + const refused = await publish(headers); + expect(refused.status).toBe(401); + expect(refused.headers["www-authenticate"]).toBe("OTP"); + expect(refused.json).toEqual({ error: otpMessage }); + } + expect(await registry.packages.has("guarded")).toBe(false); + + expect((await publish({ "npm-otp": "123456" })).status).toBe(200); + expect(await registry.packages.has("guarded")).toBe(true); + }); + + test("a read does not need the code", async () => { + const { registry, token } = await setup(); + using _ = registry; + expect((await request(`${registry.url}-/whoami`, { headers: jsonHeaders(token) })).status).toBe(200); + }); + + test("an automation token passes", async () => { + const { registry, user, publish } = await setup(); + using _ = registry; + const automation = registry.auth.createToken(user, { automation: true }).token; + expect((await publish({}, automation)).status).toBe(200); + }); + + test("the web flow hands out a code for one write", async () => { + const { registry, publish } = await setup(); + using _ = registry; + const origin = registry.url.slice(0, -1); + + const refused = await publish({ "npm-auth-type": "web" }); + expect(refused.status).toBe(401); + expect(refused.headers["www-authenticate"]).toBe("OTP"); + expect(refused.json).toEqual({ + error: otpMessage, + authUrl: expect.stringMatching(new RegExp(`^${origin}/auth/cli/[0-9a-f-]{36}$`)), + doneUrl: expect.stringMatching(new RegExp(`^${origin}/-/v1/done\\?authId=[0-9a-f-]{36}$`)), + }); + + const waiting = await request(refused.json.doneUrl); + expect(waiting).toMatchObject({ status: 202, headers: { "retry-after": "1" } }); + + // The user opens authUrl in a browser and logs in there. + const approved = await request(refused.json.authUrl, { + headers: { authorization: `Basic ${btoa("olga:secret")}` }, + }); + expect(approved.status).toBe(200); + + const done = await request(refused.json.doneUrl); + expect(done.status).toBe(200); + expect(done.json).toEqual({ token: expect.any(String) }); + + expect((await publish({ "npm-otp": done.json.token, "npm-auth-type": "legacy" })).status).toBe(200); + // The code does not work a second time. + registry.packages.delete("guarded"); + expect((await publish({ "npm-otp": done.json.token })).status).toBe(401); + }); + + test("a login needs the code", async () => { + const { registry } = await setup(); + using _ = registry; + const refused = await login(registry, "olga", { password: "secret" }); + expect(refused).toMatchObject({ status: 401, headers: { "www-authenticate": "OTP" } }); + const accepted = await login(registry, "olga", { password: "secret" }, { "npm-otp": "123456" }); + expect(accepted.status).toBe(201); + }); +}); + +describe("web login", () => { + test("the client waits until the user approves", async () => { + using registry = new Registry().start(); + registry.auth.addUser("peggy", "secret"); + const origin = registry.url.slice(0, -1); + + const opened = await request(`${registry.url}-/v1/login`, { + method: "POST", + headers: jsonHeaders(), + body: "{}", + }); + expect(opened.status).toBe(200); + expect(opened.json).toEqual({ + loginUrl: expect.stringMatching(new RegExp(`^${origin}/login/cli/[0-9a-f-]{36}$`)), + doneUrl: expect.stringMatching(new RegExp(`^${origin}/-/v1/done\\?sessionId=[0-9a-f-]{36}$`)), + }); + + expect((await request(opened.json.doneUrl)).status).toBe(202); + // The page refuses a visitor that is not logged in. + expect((await request(opened.json.loginUrl)).status).toBe(401); + expect((await request(opened.json.doneUrl)).status).toBe(202); + + const sessionId = new URL(opened.json.doneUrl).searchParams.get("sessionId")!; + registry.auth.approveSession(sessionId, "peggy"); + + const done = await request(opened.json.doneUrl); + expect(done.status).toBe(200); + const whoami = await request(`${registry.url}-/whoami`, { headers: jsonHeaders(done.json.token) }); + expect(whoami.json).toEqual({ username: "peggy" }); + + // The token is handed out once. + expect((await request(opened.json.doneUrl)).status).toBe(404); + for (const query of ["?sessionId=unknown", "?authId=unknown", ""]) { + const reply = await request(`${registry.url}-/v1/done${query}`); + expect({ status: reply.status, text: reply.text }).toEqual({ status: 404, text: `{"message":"not found"}` }); + } + }); +}); diff --git a/test/packages/registry/test/cli.test.ts b/test/packages/registry/test/cli.test.ts new file mode 100644 index 000000000000..c3661d51d0a7 --- /dev/null +++ b/test/packages/registry/test/cli.test.ts @@ -0,0 +1,377 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import { join } from "node:path"; +import { TestRegistry, toml } from "../test-registry.ts"; +import { abbreviatedAccept, jsonHeaders, pack, publishBody, request, type Manifest } from "./helpers.ts"; + +/** A project that installs from and publishes to the registry, as `user` when one is given. */ +function project(registry: TestRegistry, manifest: Record, user?: string) { + const token = user === undefined ? undefined : registry.auth.createToken(registry.auth.users.get(user)!).token; + return tempDir("registry-cli-", { + "package.json": JSON.stringify(manifest), + "bunfig.toml": toml({ + install: { cache: false, registry: token === undefined ? registry.url : { url: registry.url, token } }, + }), + }); +} + +/** Puts a package into the registry over HTTP, for a test that is about what bun does with it afterwards. */ +async function seed(registry: TestRegistry, manifest: Manifest, user: string) { + const { token } = registry.auth.createToken(registry.auth.users.get(user)!); + const reply = await request(registry.url + manifest.name.replace("/", "%2f"), { + method: "PUT", + headers: jsonHeaders(token), + body: JSON.stringify(publishBody(manifest, await pack(manifest), { access: "public" })), + }); + expect(reply.status).toBe(200); +} + +async function bun(cwd: string, ...args: string[]) { + await using proc = Bun.spawn({ cmd: [bunExe(), ...args], cwd, env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; +} + +describe.concurrent("bun against the registry", () => { + test("install", async () => { + using registry = new TestRegistry({ recordRequests: true }).start(); + using dir = project(registry, { + name: "app", + dependencies: { "no-deps": "^1.0.0", "@types/is-number": "1.0.0" }, + }); + + const { stderr, exitCode } = await bun(String(dir), "install", "--save-text-lockfile"); + expect(stderr).not.toContain("error:"); + expect(exitCode).toBe(0); + + expect(await Bun.file(join(String(dir), "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.1.0", + }); + const lockfile = await Bun.file(join(String(dir), "bun.lock")).text(); + expect(lockfile).toContain(`"@types/is-number": ["@types/is-number@1.0.0", "`); + + expect(registry.requests.map(({ method, path, status }) => `${status} ${method} ${path}`).sort()).toEqual([ + "200 GET /@types%2fis-number", + "200 GET /@types/is-number/-/is-number-1.0.0.tgz", + "200 GET /no-deps", + "200 GET /no-deps/-/no-deps-1.1.0.tgz", + ]); + for (const { path, headers } of registry.requests) { + if (!path.endsWith(".tgz")) expect(headers.accept).toBe(abbreviatedAccept); + } + }); + + test("install a restricted package without a user", async () => { + using registry = new TestRegistry().start(); + using dir = project(registry, { name: "app", dependencies: { "@needs-auth/test-pkg": "1.0.0" } }); + + const refused = await bun(String(dir), "install"); + expect(refused.stderr).toContain(`GET ${registry.url}@needs-auth%2ftest-pkg - 404`); + expect(refused.exitCode).toBe(1); + }); + + test("install a restricted package as a user", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("reader", "secret"); + using dir = project(registry, { name: "app", dependencies: { "@needs-auth/test-pkg": "1.0.0" } }, "reader"); + + const installed = await bun(String(dir), "install"); + expect(installed.stderr).not.toContain("error:"); + expect(installed.exitCode).toBe(0); + expect( + await Bun.file(join(String(dir), "node_modules", "@needs-auth", "test-pkg", "package.json")).json(), + ).toMatchObject({ name: "@needs-auth/test-pkg", version: "1.0.0" }); + }); + + test("publish", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("author", "secret", { email: "author@example.com" }); + using dir = project( + registry, + { + name: "@author/published", + version: "1.2.3", + description: "published by a test", + dependencies: { "no-deps": "1.0.0" }, + scripts: { postinstall: "echo installed" }, + }, + "author", + ); + await Bun.write(join(String(dir), "README.md"), "# published\n"); + + const published = await bun(String(dir), "publish", "--access", "public", "--tag", "beta"); + expect(published.stderr).not.toContain("error:"); + expect(published.stdout).toContain("+ @author/published@1.2.3"); + expect(published.exitCode).toBe(0); + + const document = (await request(`${registry.url}@author%2fpublished`)).json; + expect(document["dist-tags"]).toEqual({ beta: "1.2.3", latest: "1.2.3" }); + expect(document.maintainers).toEqual([{ name: "author", email: "author@example.com" }]); + expect(document.readme).toBe("# published\n"); + expect(document.versions["1.2.3"].dist.tarball).toBe(`${registry.url}@author/published/-/published-1.2.3.tgz`); + const abbreviated = ( + await request(`${registry.url}@author%2fpublished`, { headers: { accept: abbreviatedAccept } }) + ).json; + expect(abbreviated.versions["1.2.3"]).toEqual({ + name: "@author/published", + version: "1.2.3", + dependencies: { "no-deps": "1.0.0" }, + dist: document.versions["1.2.3"].dist, + hasInstallScript: true, + }); + }); + + test("publish a version with build metadata", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("author", "secret"); + using dir = project(registry, { name: "built-by-bun", version: "1.0.0+build.5" }, "author"); + + const published = await bun(String(dir), "publish"); + expect(published.stderr).not.toContain("error:"); + expect(published.exitCode).toBe(0); + + const document = (await request(`${registry.url}built-by-bun`)).json; + expect(Object.keys(document.versions)).toEqual(["1.0.0"]); + expect(document["dist-tags"]).toEqual({ latest: "1.0.0" }); + }); + + test("pm view", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("author", "secret", { email: "author@example.com" }); + await seed(registry, { name: "viewed", version: "1.2.3", description: "seen by a test" }, "author"); + using dir = project(registry, { name: "app" }); + + const view = await bun(String(dir), "pm", "view", "viewed", "--json"); + expect(view.stderr).not.toContain("error:"); + expect(JSON.parse(view.stdout)).toMatchObject({ + name: "viewed", + version: "1.2.3", + description: "seen by a test", + dist: { tarball: `${registry.url}viewed/-/viewed-1.2.3.tgz` }, + maintainers: [{ name: "author", email: "author@example.com" }], + versions: ["1.2.3"], + }); + expect(view.exitCode).toBe(0); + }); + + test("install what was published", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("author", "secret"); + const manifest = { name: "@author/installed", version: "1.2.3", dependencies: { "no-deps": "1.0.0" } }; + await seed(registry, manifest, "author"); + await seed(registry, { ...manifest, version: "2.0.0-rc.1" }, "author"); + + using consumer = project(registry, { name: "app", dependencies: { "@author/installed": "^1.0.0" } }); + const installed = await bun(String(consumer), "install"); + expect(installed.stderr).not.toContain("error:"); + expect(installed.exitCode).toBe(0); + const modules = join(String(consumer), "node_modules"); + expect(await Bun.file(join(modules, "@author", "installed", "package.json")).json()).toEqual(manifest); + expect(await Bun.file(join(modules, "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + }); + }); + + test("publish a version that is there", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("author", "secret"); + const manifest = { name: "published-twice", version: "1.2.3" }; + await seed(registry, manifest, "author"); + using dir = project(registry, manifest, "author"); + + const again = await bun(String(dir), "publish"); + expect(again.stderr).toContain("403 Forbidden"); + expect(again.stderr).toContain("You cannot publish over the previously published versions: 1.2.3."); + expect(again.exitCode).toBe(1); + }); + + test("publish --tolerate-republish of a version that is there", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("author", "secret"); + const manifest = { name: "tolerated", version: "1.2.3" }; + await seed(registry, manifest, "author"); + using dir = project(registry, manifest, "author"); + + const tolerated = await bun(String(dir), "publish", "--tolerate-republish"); + expect(tolerated.stderr).toBe("warn: Registry already knows about version 1.2.3; skipping.\n"); + expect(tolerated.exitCode).toBe(0); + }); + + test("whoami", async () => { + using registry = new TestRegistry({ notice: "This registry is for tests." }).start(); + registry.auth.addUser("somebody", "secret"); + using dir = project(registry, { name: "app" }, "somebody"); + + const known = await bun(String(dir), "pm", "whoami"); + expect(known.stdout).toBe("somebody\n"); + expect(known.stderr).toContain("This registry is for tests."); + expect(known.exitCode).toBe(0); + }); + + test("whoami with a token that the registry does not know", async () => { + using registry = new TestRegistry().start(); + using dir = tempDir("registry-cli-", { + "package.json": JSON.stringify({ name: "app" }), + "bunfig.toml": toml({ + install: { cache: false, registry: { url: registry.url, token: "npm_notATokenHere" } }, + }), + }); + + const unknown = await bun(String(dir), "pm", "whoami"); + expect(unknown.stdout).toBe(""); + expect(unknown.stderr).toBe(`\n401 Unauthorized: ${registry.url}-/whoami\n`); + expect(unknown.exitCode).toBe(1); + }); + + test("publish with --otp", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("careful", "secret", { tfa: "auth-and-writes", otp: ["246810"] }); + using dir = project(registry, { name: "with-otp", version: "1.0.0" }, "careful"); + + const right = await bun(String(dir), "publish", "--otp", "246810"); + expect(right.stderr).not.toContain("error:"); + expect(right.stdout).toContain("+ with-otp@1.0.0"); + expect(right.exitCode).toBe(0); + expect(await registry.packages.has("with-otp")).toBe(true); + }); + + test("publish with an --otp that is wrong", async () => { + using registry = new TestRegistry().start(); + registry.auth.addUser("careful", "secret", { tfa: "auth-and-writes", otp: ["246810"] }); + using dir = project(registry, { name: "with-wrong-otp", version: "1.0.0" }, "careful"); + + // bun asks for another code when the registry refuses the one it sent. Nobody is there to type it. + const wrong = await bun(String(dir), "publish", "--otp", "000000"); + expect(wrong.stderr).toContain("failed to read OTP input"); + expect(wrong.exitCode).toBe(1); + expect(await registry.packages.has("with-wrong-otp")).toBe(false); + }); + + test("publish waits for the user to approve in the browser", async () => { + const opened = Promise.withResolvers(); + using registry = new TestRegistry({ + intercept(request, registry) { + // The first poll of doneUrl tells the test that bun has the session. + const url = new URL(request.url); + if (url.pathname !== "/-/v1/done") return; + const id = url.searchParams.get("authId")!; + if (registry.auth.sessions.get(id)?.result === null) opened.resolve(id); + }, + }).start(); + registry.auth.addUser("browser", "secret", { tfa: "auth-and-writes" }); + using dir = project(registry, { name: "with-web-auth", version: "1.0.0" }, "browser"); + + const publishing = bun(String(dir), "publish", "--auth-type", "web"); + const exitedEarly = publishing.then(({ stderr, exitCode }) => { + throw new Error(`bun publish exited with code ${exitCode} before it asked for the session:\n${stderr}`); + }); + registry.auth.approveSession(await Promise.race([opened.promise, exitedEarly])); + const { stdout, stderr, exitCode } = await publishing; + expect(stderr).not.toContain("error:"); + expect(stdout).toContain(`${registry.url}auth/cli/`); + expect(stdout).toContain("+ with-web-auth@1.0.0"); + expect(exitCode).toBe(0); + expect(await registry.packages.has("with-web-auth")).toBe(true); + }); + + test("audit reports an advisory", async () => { + using registry = new TestRegistry().start(); + registry.advisories.add("no-deps", { vulnerable_versions: "<1.1.0", severity: "high", title: "no-deps is unsafe" }); + using dir = project(registry, { name: "app", dependencies: { "no-deps": "1.0.0", "a-dep": "1.0.1" } }); + expect((await bun(String(dir), "install")).exitCode).toBe(0); + + const { stdout, exitCode } = await bun(String(dir), "audit"); + expect(stdout).toContain("no-deps@1.0.0\n"); + expect(stdout).toContain("high: no-deps is unsafe (<1.1.0) - https://github.com/advisories/GHSA-1000000"); + expect(stdout).toContain("1 vulnerability (1 high)"); + expect(exitCode).toBe(1); + }); +}); + +describe("cli.ts", () => { + test("serves a directory", async () => { + using registry = new TestRegistry(); + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + join(import.meta.dir, "..", "cli.ts"), + "--port=0", + `--storage=${registry.packagesPath}`, + "--user=someone:secret", + "--restricted=@needs-auth/*", + ], + env: bunEnv, + stdout: "pipe", + stderr: "inherit", + }); + + let output = ""; + const decoder = new TextDecoder(); + for await (const chunk of proc.stdout) { + output += decoder.decode(chunk, { stream: true }); + if (/^registry: .+\n/m.test(output)) break; + } + const token = output.match(/^token for someone: (npm_[0-9A-Za-z]{36})$/m)![1]; + const url = output.match(/^registry: (http:\/\/localhost:\d+\/)$/m)![1]; + + expect((await request(`${url}no-deps`)).status).toBe(200); + expect((await request(`${url}@needs-auth%2ftest-pkg`)).status).toBe(404); + const authorized = await request(`${url}@needs-auth%2ftest-pkg`, { headers: { authorization: `Bearer ${token}` } }); + expect(authorized.status).toBe(200); + }); + + /** What the program prints and how it exits, for arguments that it refuses. */ + async function refused(...args: string[]) { + await using proc = Bun.spawn({ + cmd: [bunExe(), join(import.meta.dir, "..", "cli.ts"), ...args], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // The message, an empty line, and the usage. + const [message, empty, usage] = stderr.split("\n"); + return { stdout, message, empty, usage, exitCode }; + } + const refusal = (message: string) => ({ + stdout: "", + message, + empty: "", + usage: "Usage: bun cli.ts [options]", + exitCode: 1, + }); + + test.concurrent("refuses a storage that is not a directory", async () => { + using dir = tempDir("registry-cli-", {}); + const missing = join(String(dir), "pakcages"); + expect(await refused("--port=0", `--storage=${missing}`)).toEqual( + refusal(`The storage is not a directory: ${missing}`), + ); + }); + + test.concurrent("refuses a port that has a registry", async () => { + using registry = new TestRegistry().start(); + expect(await refused(`--port=${registry.port}`)).toEqual( + refusal(`Failed to start server. Is port ${registry.port} in use?`), + ); + }); + + test.concurrent.each([ + [["--port=70000"], `--port must be a number from 0 to 65535, got "70000"`], + [["--port=-1"], `--port must be a number from 0 to 65535, got "-1"`], + [["--port="], `--port must be a number from 0 to 65535, got ""`], + [["--port= "], `--port must be a number from 0 to 65535, got " "`], + [["--port=0x50"], `--port must be a number from 0 to 65535, got "0x50"`], + [["--port=1e3"], `--port must be a number from 0 to 65535, got "1e3"`], + [["--port=0", "--user=someone"], `--user must be name:password, got "someone"`], + [["--port=0", "--user=someone:"], "A password is required"], + [["--port=0", "--user=some/one:secret"], "Name may not contain non-url-safe chars"], + // No token is printed for the first user when the program refuses the second one. + [["--port=0", "--user=someone:secret", "--user=someone:other"], "user someone already exists"], + [["--port=0", "--storag=packages"], "Unknown option '--storag'"], + ])("refuses %j", async (args, message) => { + expect(await refused(...args)).toEqual(refusal(message)); + }); +}); diff --git a/test/packages/registry/test/helpers.ts b/test/packages/registry/test/helpers.ts new file mode 100644 index 000000000000..6fb4c1767f85 --- /dev/null +++ b/test/packages/registry/test/helpers.ts @@ -0,0 +1,163 @@ +import { tempDir, type DirectoryTree } from "harness"; +import { brotliDecompressSync } from "node:zlib"; +import type { Packument, VersionDocument } from "../index.ts"; + +/** What bun and npm send to ask for the abbreviated packument. */ +export const abbreviatedAccept = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*"; + +export type Manifest = { name: string; version: string } & Record; + +const packed = new Map>(); + +/** + * The tarball of a package. The archive holds the time at which it was made, so two archives of the same files can + * differ. The same input gives the same bytes here: the first archive is kept. + */ +export function pack(manifest: Manifest, files: Record = {}): Promise { + const entries: Record = { "package/package.json": JSON.stringify(manifest) }; + for (const [path, content] of Object.entries(files)) entries[`package/${path}`] = content; + const key = JSON.stringify(entries); + let tarball = packed.get(key); + if (tarball === undefined) { + tarball = new Bun.Archive(entries, { compress: "gzip" }).bytes().then(bytes => Buffer.from(bytes)); + packed.set(key, tarball); + } + return tarball; +} + +export const sha1 = (bytes: Uint8Array) => new Bun.CryptoHasher("sha1").update(bytes).digest("hex"); +export const sha512 = (bytes: Uint8Array) => `sha512-${new Bun.CryptoHasher("sha512").update(bytes).digest("base64")}`; +export const md5 = (bytes: Uint8Array | string) => new Bun.CryptoHasher("md5").update(bytes).digest("hex"); + +/** The version object as a registry stores it. The host of the tarball URL is stale on purpose. */ +export function stored(manifest: Manifest, tarball: Uint8Array): VersionDocument { + return { + ...manifest, + _id: `${manifest.name}@${manifest.version}`, + dist: { + integrity: sha512(tarball), + shasum: sha1(tarball), + tarball: `http://localhost:4873/${manifest.name}/-/${manifest.name}-${manifest.version}.tgz`, + }, + }; +} + +export interface Fixture { + manifests: Manifest[]; + tags?: Record; + /** `false` leaves `time` out of the packument, as a hand-written fixture does. */ + time?: false; + /** Versions that are in the packument but whose tarball is not on disk. */ + withoutTarball?: string[]; + extra?: Record; +} + +/** A storage directory in the layout of verdaccio: `/package.json` next to `-.tgz`. */ +export async function storage(fixtures: Fixture[]) { + const tree: DirectoryTree = {}; + const tarballs = new Map(); + + for (const { manifests, tags, time, withoutTarball = [], extra } of fixtures) { + const name = manifests[0].name; + const basename = name.slice(name.indexOf("/") + 1); + const versions: Record = {}; + const times: Record = { + created: "2024-01-01T00:00:00.000Z", + modified: "2024-03-02T10:20:30.400Z", + }; + for (const [index, manifest] of manifests.entries()) { + const tarball = await pack(manifest); + versions[manifest.version] = stored(manifest, tarball); + times[manifest.version] = `2024-01-0${index + 1}T00:00:00.000Z`; + if (withoutTarball.includes(manifest.version)) continue; + tree[`${name}/${basename}-${manifest.version}.tgz`] = tarball; + tarballs.set(`${name}@${manifest.version}`, tarball); + } + const packument: Packument = { + _id: name, + name, + "dist-tags": tags ?? { latest: manifests.at(-1)!.version }, + versions, + ...(time === false ? {} : { time: times }), + // What verdaccio adds to a stored document. The registry must not serve it. + _attachments: {}, + _distfiles: {}, + _uplinks: {}, + _rev: "3-0123456789abcdef", + ...extra, + }; + tree[`${name}/package.json`] = JSON.stringify(packument, null, 2); + } + + const directory = tempDir("registry-storage-", tree); + return { directory, path: String(directory), tarballs }; +} + +export interface Reply { + status: number; + headers: Record; + text: string; + json: any; + bytes: Uint8Array; +} + +/** Sends one request and reads the answer whole. The body stays as the server encoded it. */ +export async function request(url: string | URL, init: RequestInit = {}): Promise { + const response = await fetch(url, { ...init, decompress: false } as RequestInit); + const bytes = await response.bytes(); + const headers = Object.fromEntries(response.headers); + delete headers.date; + const encoding = headers["content-encoding"]; + const plain = + encoding === "gzip" + ? Bun.gunzipSync(bytes) + : encoding === "br" + ? new Uint8Array(brotliDecompressSync(bytes)) + : bytes; + const text = new TextDecoder().decode(plain); + let json: unknown; + try { + json = JSON.parse(text); + } catch {} + return { status: response.status, headers, text, json, bytes: plain }; +} + +export function publishBody( + manifest: Manifest, + tarball: Uint8Array, + options: { tag?: string; access?: "public" | "restricted" | null; registry?: string } = {}, +) { + const { name, version } = manifest; + const host = new URL(options.registry ?? "http://localhost:4873/").host; + return { + _id: name, + name, + "dist-tags": { [options.tag ?? "latest"]: version }, + versions: { + [version]: { + ...manifest, + _id: `${name}@${version}`, + _nodeVersion: "24.3.0", + _npmVersion: "10.8.3", + dist: { + integrity: sha512(tarball), + shasum: sha1(tarball), + tarball: `http://${host}/${name}/-/${name}-${version}.tgz`, + }, + }, + }, + access: options.access ?? null, + _attachments: { + [`${name}-${version}.tgz`]: { + content_type: "application/octet-stream", + data: Buffer.from(tarball).toString("base64"), + length: tarball.byteLength, + }, + }, + }; +} + +export const jsonHeaders = (token?: string): Record => ({ + "content-type": "application/json", + ...(token === undefined ? {} : { authorization: `Bearer ${token}` }), +}); diff --git a/test/packages/registry/test/publish.test.ts b/test/packages/registry/test/publish.test.ts new file mode 100644 index 000000000000..7fa6f6fcb498 --- /dev/null +++ b/test/packages/registry/test/publish.test.ts @@ -0,0 +1,662 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { Registry } from "../index.ts"; +import type { Manifest } from "./helpers.ts"; +import { abbreviatedAccept, jsonHeaders, pack, publishBody, request, sha1, sha512, storage } from "./helpers.ts"; + +let fixtures: Awaited>; + +beforeAll(async () => { + fixtures = await storage([ + { + manifests: [ + { name: "on-disk", version: "1.0.0" }, + { name: "on-disk", version: "1.1.0" }, + ], + }, + ]); +}); + +afterAll(() => { + fixtures.directory[Symbol.dispose](); +}); + +function setup(options: ConstructorParameters[0] = {}) { + const registry = new Registry({ storage: fixtures.path, ...options }).start(); + const token = (name: string) => + registry.auth.createToken(registry.auth.users.get(name) ?? registry.auth.addUser(name, "secret")).token; + const put = (path: string, body: unknown, user: string | null = "alice", headers: Record = {}) => + request(registry.url + path, { + method: "PUT", + headers: { ...jsonHeaders(user === null ? undefined : token(user)), ...headers }, + body: JSON.stringify(body), + }); + const publish = async (manifest: Manifest, options: Parameters[2] & { user?: string } = {}) => + put( + manifest.name.replace("/", "%2f"), + publishBody(manifest, await pack(manifest), { registry: registry.url, ...options }), + options.user, + ); + const packument = async (name: string, user?: string) => + ( + await request(registry.url + name.replace("/", "%2f") + "?write=true", { + headers: user === undefined ? {} : { authorization: `Bearer ${token(user)}` }, + }) + ).json; + return { registry, token, put, publish, packument }; +} + +describe("publish", () => { + test("a new package", async () => { + const { registry, publish } = setup(); + using _ = registry; + const manifest = { + name: "fresh", + version: "1.0.0", + description: "a fresh package", + keywords: ["new"], + license: "MIT", + scripts: { postinstall: "echo hi" }, + readme: "# fresh", + readmeFilename: "README.md", + }; + const tarball = await pack(manifest); + const before = Date.now(); + const reply = await publish(manifest); + expect(reply.status).toBe(200); + expect(reply.json).toEqual({ ok: true, id: "fresh", rev: expect.stringMatching(/^1-[0-9a-f]{32}$/) }); + + const full = await request(`${registry.url}fresh`); + const published = new Date(full.json.time["1.0.0"]).getTime(); + expect(published).toBeGreaterThanOrEqual(before); + expect(full.json).toEqual({ + _id: "fresh", + _rev: reply.json.rev, + name: "fresh", + "dist-tags": { latest: "1.0.0" }, + versions: { + "1.0.0": { + name: "fresh", + version: "1.0.0", + description: "a fresh package", + keywords: ["new"], + license: "MIT", + scripts: { postinstall: "echo hi" }, + readmeFilename: "README.md", + _id: "fresh@1.0.0", + _nodeVersion: "24.3.0", + _npmVersion: "10.8.3", + dist: { + integrity: sha512(tarball), + shasum: sha1(tarball), + // The registry decides where the tarball is, not the client. + tarball: `${registry.url}fresh/-/fresh-1.0.0.tgz`, + }, + _npmUser: { name: "alice", email: "alice@example.com" }, + maintainers: [{ name: "alice", email: "alice@example.com" }], + }, + }, + maintainers: [{ name: "alice", email: "alice@example.com" }], + time: { "1.0.0": full.json.time["1.0.0"], modified: full.json.time["1.0.0"], created: full.json.time["1.0.0"] }, + // The fields of the latest version that the registry lifts to the top. + description: "a fresh package", + keywords: ["new"], + license: "MIT", + readmeFilename: "README.md", + readme: "# fresh", + }); + + const abbreviated = await request(`${registry.url}fresh`, { headers: { accept: abbreviatedAccept } }); + expect(abbreviated.json.versions["1.0.0"].hasInstallScript).toBe(true); + expect(abbreviated.json.modified).toBe(full.json.time.modified); + + const download = await request(full.json.versions["1.0.0"].dist.tarball); + expect(Buffer.from(download.bytes).equals(tarball)).toBe(true); + expect((await request(`${registry.url}-/package/fresh/collaborators`)).json).toEqual({ alice: "write" }); + }); + + test("more versions and tags", async () => { + const { registry, publish, packument } = setup(); + using _ = registry; + expect((await publish({ name: "tagged", version: "1.0.0" }, { tag: "next" })).status).toBe(200); + // Every package has a latest. The first version takes it when the client asks for another tag only. + expect((await packument("tagged"))["dist-tags"]).toEqual({ next: "1.0.0", latest: "1.0.0" }); + + const second = await publish({ name: "tagged", version: "2.0.0-rc.1" }, { tag: "next" }); + expect(second.json.rev).toStartWith("2-"); + expect((await packument("tagged"))["dist-tags"]).toEqual({ next: "2.0.0-rc.1", latest: "1.0.0" }); + + expect((await publish({ name: "tagged", version: "0.9.0", description: "old" })).status).toBe(200); + const document = await packument("tagged"); + // The registry does not compare versions: the client put latest on 0.9.0. + expect(document["dist-tags"]).toEqual({ next: "2.0.0-rc.1", latest: "0.9.0" }); + expect(Object.keys(document.versions)).toEqual(["1.0.0", "2.0.0-rc.1", "0.9.0"]); + expect(document.description).toBe("old"); + expect(document.time.created).toBe(document.time["1.0.0"]); + expect(document.time.modified).toBe(document.time["0.9.0"]); + }); + + test("a version of a package of the storage directory", async () => { + const { registry, publish, packument } = setup(); + using _ = registry; + expect((await publish({ name: "on-disk", version: "2.0.0" })).status).toBe(200); + const document = await packument("on-disk"); + expect(Object.keys(document.versions)).toEqual(["1.0.0", "1.1.0", "2.0.0"]); + expect(document["dist-tags"]).toEqual({ latest: "2.0.0" }); + for (const version of Object.values(document.versions)) { + const download = await request(version.dist.tarball); + expect(sha512(download.bytes)).toBe(version.dist.integrity); + } + // The directory is as it was. Another registry on the same storage does not see the version. + using other = new Registry({ storage: fixtures.path }).start(); + expect(Object.keys((await request(`${other.url}on-disk`)).json.versions)).toEqual(["1.0.0", "1.1.0"]); + registry.packages.reset("on-disk"); + expect(Object.keys((await packument("on-disk")).versions)).toEqual(["1.0.0", "1.1.0"]); + }); + + test("a version is published once", async () => { + const { registry, publish } = setup(); + using _ = registry; + expect((await publish({ name: "once", version: "1.0.0" })).status).toBe(200); + for (const name of ["once", "on-disk"]) { + const again = await publish({ name, version: "1.0.0", description: "changed" }); + expect(again.status).toBe(403); + expect(again.json).toEqual({ error: "You cannot publish over the previously published versions: 1.0.0." }); + } + expect((await request(`${registry.url}once`)).json).not.toHaveProperty("description"); + + // A test that needs the same version again removes the package. + registry.packages.delete("once"); + expect((await request(`${registry.url}once`)).status).toBe(404); + expect((await publish({ name: "once", version: "1.0.0" })).status).toBe(200); + }); + + test("concurrent publishes of one version", async () => { + const { registry, publish, packument, token } = setup(); + using _ = registry; + token("alice"); + const replies = await Promise.all( + Array.from({ length: 8 }, (_, index) => publish({ name: "race", version: "1.0.0", description: String(index) })), + ); + expect(replies.map(reply => reply.status).sort()).toEqual([200, 403, 403, 403, 403, 403, 403, 403]); + const versions = await Promise.all( + Array.from({ length: 8 }, (_, index) => publish({ name: "many", version: `1.0.${index}` })), + ); + expect(versions.map(reply => reply.status)).toEqual(Array(8).fill(200)); + expect(Object.keys((await packument("many")).versions)).toHaveLength(8); + }); + + test("who can publish", async () => { + const { registry, publish, put } = setup(); + using _ = registry; + const manifest = { name: "owned", version: "1.0.0" }; + const body = publishBody(manifest, await pack(manifest)); + + const anonymous = await put("owned", body, null); + expect(anonymous.status).toBe(401); + expect(anonymous.json).toEqual({ error: "You must be logged in to publish packages." }); + + const wrongToken = await request(`${registry.url}owned`, { + method: "PUT", + headers: jsonHeaders("npm_000000000000000000000000000000000000"), + body: JSON.stringify(body), + }); + expect(wrongToken.status).toBe(401); + + expect((await publish(manifest, { user: "alice" })).status).toBe(200); + const stranger = await publish({ name: "owned", version: "1.0.1" }, { user: "bob" }); + expect(stranger.status).toBe(403); + expect(stranger.json).toEqual({ + error: `You do not have permission to publish "owned". Are you logged in as the correct user?`, + }); + // A package of the storage directory has no maintainers, so every user can add a version. + expect((await publish({ name: "on-disk", version: "3.0.0" }, { user: "bob" })).status).toBe(200); + }); + + test("access rules replace the defaults", async () => { + const { registry, publish } = setup({ + access: { "@team/*": { read: "authenticated", write: ["alice"] }, "open-*": { write: "authenticated" } }, + }); + using _ = registry; + expect((await publish({ name: "@team/tool", version: "1.0.0" }, { user: "bob" })).status).toBe(403); + expect((await publish({ name: "@team/tool", version: "1.0.0" }, { user: "alice", access: "public" })).status).toBe( + 200, + ); + expect((await request(`${registry.url}@team%2ftool`)).status).toBe(404); + + expect((await publish({ name: "open-source", version: "1.0.0" }, { user: "alice" })).status).toBe(200); + expect((await publish({ name: "open-source", version: "1.0.1" }, { user: "bob" })).status).toBe(200); + }); + + test("a scoped package is restricted unless the client says public", async () => { + const { registry, publish, token } = setup(); + using _ = registry; + expect((await publish({ name: "@alice/secret", version: "1.0.0" })).status).toBe(200); + expect((await publish({ name: "@alice/open", version: "1.0.0" }, { access: "public" })).status).toBe(200); + expect((await publish({ name: "@alice/closed", version: "1.0.0" }, { access: "restricted" })).status).toBe(200); + expect((await publish({ name: "unscoped", version: "1.0.0" }, { access: "restricted" })).status).toBe(400); + + const statuses = async (authorization?: string) => + Object.fromEntries( + await Promise.all( + ["@alice%2fsecret", "@alice%2fopen", "@alice%2fclosed"].map(async path => [ + path, + (await request(registry.url + path, { headers: authorization ? { authorization } : {} })).status, + ]), + ), + ); + expect(await statuses()).toEqual({ "@alice%2fsecret": 404, "@alice%2fopen": 200, "@alice%2fclosed": 404 }); + expect(await statuses(`Bearer ${token("alice")}`)).toEqual({ + "@alice%2fsecret": 200, + "@alice%2fopen": 200, + "@alice%2fclosed": 200, + }); + // Another user is not a maintainer. + expect(await statuses(`Bearer ${token("bob")}`)).toEqual({ + "@alice%2fsecret": 404, + "@alice%2fopen": 200, + "@alice%2fclosed": 404, + }); + + const visibility = `${registry.url}-/package/@alice%2fsecret/visibility`; + const headers = jsonHeaders(token("alice")); + expect((await request(visibility, { headers })).json).toEqual({ public: false }); + const opened = await request(`${registry.url}-/package/@alice%2fsecret/access`, { + method: "POST", + headers, + body: JSON.stringify({ access: "public" }), + }); + expect(opened.status).toBe(200); + expect((await request(visibility)).json).toEqual({ public: true }); + expect((await request(`${registry.url}@alice%2fsecret`)).status).toBe(200); + }); + + test.each([ + ["another name in the body", (body: any) => (body.name = "other"), "the body is for"], + ["another name in the version", (body: any) => (body.versions["1.0.0"].name = "other"), "must have this name"], + ["another version in the version", (body: any) => (body.versions["1.0.0"].version = "1.0.1"), "must have this"], + ["two versions", (body: any) => (body.versions["1.0.1"] = body.versions["1.0.0"]), "one version"], + ["no versions", (body: any) => (body.versions = {}), "one version"], + ["a wrong shasum", (body: any) => (body.versions["1.0.0"].dist.shasum = "0".repeat(40)), "dist.shasum"], + [ + "a wrong integrity", + (body: any) => (body.versions["1.0.0"].dist.integrity = `sha512-${btoa("0".repeat(64))}`), + "dist.integrity", + ], + ["a wrong length", (body: any) => (Object.values(body._attachments)[0].length += 1), "length"], + ["data that is not base64", (body: any) => (Object.values(body._attachments)[0].data = "%%%"), "base64"], + ["an empty tarball", (body: any) => (Object.values(body._attachments)[0].data = ""), "empty"], + ["a tag that reads as a range", (body: any) => (body["dist-tags"] = { "1.x": "1.0.0" }), "dist-tag"], + ["a tag for another version", (body: any) => (body["dist-tags"] = { latest: "2.0.0" }), "must point to"], + ["an unknown access", (body: any) => (body.access = "secret"), "access"], + ])("refuses %s", async (_, change, message) => { + const { registry, put } = setup(); + using __ = registry; + const manifest = { name: "checked", version: "1.0.0" }; + const body = publishBody(manifest, await pack(manifest)); + change(body); + const reply = await put("checked", body); + expect(reply.status).toBe(400); + expect(reply.json.error).toContain(message); + expect(await registry.packages.has("checked")).toBe(false); + }); + + test.each([ + ["Capital", "name can no longer contain capital letters"], + ["http", "http is a core module name"], + ["special~", `name can no longer contain special characters ("~'!()*")`], + ["x".repeat(215), "name can no longer contain more than 214 characters"], + ])("refuses the new name %s", async (name, reason) => { + const { registry, publish } = setup(); + using _ = registry; + const reply = await publish({ name, version: "1.0.0" }); + expect(reply.status).toBe(400); + expect(reply.json.error).toEndWith(reason); + }); + + test.each(["1.0", "v1.0.0", "01.0.0", "latest", "1.0.0.0"])("refuses the version %s", async version => { + const { registry, publish } = setup(); + using _ = registry; + const reply = await publish({ name: "versioned", version }); + expect(reply).toMatchObject({ status: 400, json: { error: `Bad Request: "${version}" is not a valid version` } }); + }); + + test("build metadata does not make another version", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + const manifest = { name: "built", version: "1.0.0+build.5" }; + + // What libnpmpublish would send if it kept the metadata: the registry has no version with a `+` in its name. + const keyed = await publish(manifest); + expect(keyed).toMatchObject({ + status: 400, + json: { error: `Bad Request: "1.0.0+build.5" is not a valid version` }, + }); + + // What bun sends: the key and the tag without the metadata, the manifest and the file name with it. + const body: any = publishBody(manifest, await pack(manifest)); + body.versions = { "1.0.0": body.versions["1.0.0+build.5"] }; + body["dist-tags"] = { latest: "1.0.0" }; + expect((await put("built", body)).status).toBe(200); + + const document = await packument("built"); + expect(Object.keys(document.versions)).toEqual(["1.0.0"]); + expect(document.versions["1.0.0"].version).toBe("1.0.0+build.5"); + expect(document.versions["1.0.0"].dist.tarball).toBe(`${registry.url}built/-/built-1.0.0.tgz`); + expect(document["dist-tags"]).toEqual({ latest: "1.0.0" }); + + // The same version with other metadata is the same version. + const other: any = publishBody({ name: "built", version: "1.0.0+build.6" }, await pack(manifest)); + other.versions = { "1.0.0": other.versions["1.0.0+build.6"] }; + other["dist-tags"] = { latest: "1.0.0" }; + expect((await put("built", other)).status).toBe(403); + }); + + test("the body must be JSON of the right type", async () => { + const { registry, token } = setup(); + using _ = registry; + const manifest = { name: "typed", version: "1.0.0" }; + const body = JSON.stringify(publishBody(manifest, await pack(manifest))); + const authorization = `Bearer ${token("alice")}`; + const send = (headers: Record, content: BodyInit = body) => + request(`${registry.url}typed`, { method: "PUT", headers: { authorization, ...headers }, body: content }); + + // The type must be `application/json`, character for character. This is the check of verdaccio, and it is + // why `bun publish` sends the header without a parameter. + for (const type of [ + "application/octet-stream", + "text/plain", + "application/json; charset=utf-8", + "application/json;charset=utf-8", + "Application/JSON", + ]) { + const refused = await send({ "content-type": type }); + expect({ type, status: refused.status }).toEqual({ type, status: 415 }); + } + const withoutType = await request(`${registry.url}typed`, { + method: "PUT", + headers: { authorization }, + body: new Blob([body]), + }); + expect(withoutType.status).toBe(415); + expect(withoutType.json.error).toBe("Unsupported Media Type: expected application/json, got no Content-Type"); + + expect((await send({ "content-type": "application/json" }, "{ not json")).status).toBe(400); + expect((await send({ "content-type": "application/json" }, "[]")).status).toBe(400); + expect(await registry.packages.has("typed")).toBe(false); + expect((await send({ "content-type": "application/json" })).status).toBe(200); + registry.packages.delete("typed"); + const gzipped = await send({ "content-type": "application/json", "content-encoding": "gzip" }, Bun.gzipSync(body)); + expect(gzipped.status).toBe(200); + }); +}); + +describe("changes after the publish", () => { + test("deprecate", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + await publish({ name: "aging", version: "1.0.0" }); + await publish({ name: "aging", version: "1.1.0" }); + + // `npm deprecate` reads the packument, writes the message into the versions, and sends all of it back. + const document = await packument("aging"); + document.versions["1.0.0"].deprecated = "1.0.0 has a bug"; + expect((await put("aging", document, "bob")).status).toBe(403); + const reply = await put("aging", document); + expect(reply.status).toBe(200); + expect(reply.json.rev).toStartWith("3-"); + + const abbreviated = await request(`${registry.url}aging`, { headers: { accept: abbreviatedAccept } }); + expect(abbreviated.json.versions["1.0.0"].deprecated).toBe("1.0.0 has a bug"); + expect(abbreviated.json.versions["1.1.0"]).not.toHaveProperty("deprecated"); + + // An empty message takes the deprecation away. + const current = await packument("aging"); + current.versions["1.0.0"].deprecated = ""; + expect((await put("aging", current)).status).toBe(200); + expect((await packument("aging")).versions["1.0.0"]).not.toHaveProperty("deprecated"); + }); + + test("a deprecation that the registry refuses changes nothing", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + await publish({ name: "aging", version: "1.0.0" }); + await publish({ name: "aging", version: "1.1.0" }); + + const document = await packument("aging"); + document.versions["1.0.0"].deprecated = "1.0.0 has a bug"; + document.versions["1.1.0"].deprecated = 5; + expect(await put("aging", document)).toMatchObject({ + status: 400, + json: { error: "Bad Request: a deprecation message is a string" }, + }); + expect((await request(`${registry.url}aging/1.0.0`)).json).not.toHaveProperty("deprecated"); + expect((await packument("aging"))._rev).toBe(document._rev); + }); + + test("star", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + await publish({ name: "shiny", version: "1.0.0" }); + // Every user can star, also one that cannot publish the package. + const document = await packument("shiny"); + const reply = await put("shiny", { _id: "shiny", _rev: document._rev, users: { bob: true } }, "bob"); + expect(reply.status).toBe(200); + expect((await packument("shiny")).users).toEqual({ bob: true }); + await put("shiny", { _id: "shiny", users: {} }, "bob"); + expect((await packument("shiny")).users).toEqual({}); + }); + + test("dist-tags", async () => { + const { registry, publish, token } = setup(); + using _ = registry; + await publish({ name: "labels", version: "1.0.0" }); + await publish({ name: "labels", version: "2.0.0" }, { tag: "next" }); + const tags = `${registry.url}-/package/labels/dist-tags`; + const write = (method: string, tag: string, version?: string, user = "alice") => + request(`${tags}/${encodeURIComponent(tag)}`, { + method, + headers: jsonHeaders(token(user)), + body: version === undefined ? undefined : JSON.stringify(version), + }); + + // The body is the version as a JSON string. + expect(await write("PUT", "stable", "1.0.0")).toMatchObject({ status: 200, json: { ok: "dist-tags updated" } }); + expect((await write("PUT", "latest", "2.0.0")).status).toBe(200); + expect((await request(tags)).json).toEqual({ latest: "2.0.0", next: "2.0.0", stable: "1.0.0" }); + expect(await write("POST", "stable", "2.0.0")).toMatchObject({ status: 405, headers: { allow: "PUT, DELETE" } }); + + expect((await write("DELETE", "next")).status).toBe(200); + expect((await request(tags)).json).toEqual({ latest: "2.0.0", stable: "1.0.0" }); + + expect(await write("PUT", "stable", "9.9.9")).toMatchObject({ status: 404 }); + expect((await write("PUT", "1.x", "1.0.0")).status).toBe(400); + expect((await write("DELETE", "latest")).status).toBe(400); + expect((await write("DELETE", "never-set")).status).toBe(404); + expect((await write("DELETE", "constructor")).status).toBe(404); + expect((await write("PUT", "__proto__", "1.0.0")).status).toBe(400); + expect((await write("PUT", "stable", "2.0.0", "bob")).status).toBe(403); + const anonymous = await request(`${tags}/stable`, { + method: "PUT", + headers: jsonHeaders(), + body: JSON.stringify("2.0.0"), + }); + expect(anonymous.status).toBe(401); + expect((await request(tags)).json).toEqual({ latest: "2.0.0", stable: "1.0.0" }); + }); +}); + +describe("unpublish", () => { + test("one version", async () => { + const { registry, publish, packument, put, token } = setup(); + using _ = registry; + await publish({ name: "shrinking", version: "1.0.0" }); + await publish({ name: "shrinking", version: "1.1.0" }, { tag: "next" }); + await publish({ name: "shrinking", version: "2.0.0" }); + + // What `npm unpublish shrinking@2.0.0` sends: the packument without the version and without its tags. + const document = await packument("shrinking"); + const tarball = new URL(document.versions["2.0.0"].dist.tarball).pathname; + delete document.versions["2.0.0"]; + delete document["dist-tags"].latest; + + const stale = await put(`shrinking/-rev/1-${"0".repeat(32)}`, document); + expect(stale).toMatchObject({ status: 409, json: { error: "Document update conflict." } }); + expect((await put(`shrinking/-rev/${document._rev}`, document, "bob")).status).toBe(403); + + const replaced = await put(`shrinking/-rev/${document._rev}`, document); + expect(replaced.status).toBe(200); + const after = await packument("shrinking"); + expect(Object.keys(after.versions)).toEqual(["1.0.0", "1.1.0"]); + // The highest version that is left takes latest. + expect(after["dist-tags"]).toEqual({ next: "1.1.0", latest: "1.1.0" }); + // The time of the version stays in the document. + expect(Object.keys(after.time)).toContain("2.0.0"); + + // The tarball goes away in a request of its own. + expect((await request(registry.url + tarball.slice(1))).status).toBe(200); + const removed = await request(`${registry.url}${tarball.slice(1)}/-rev/${after._rev}`, { + method: "DELETE", + headers: jsonHeaders(token("alice")), + }); + expect(removed.status).toBe(200); + expect((await request(registry.url + tarball.slice(1))).status).toBe(404); + + // The tarball of a version that is still there cannot be removed. + const kept = await request(`${registry.url}shrinking/-/shrinking-1.0.0.tgz/-rev/${after._rev}`, { + method: "DELETE", + headers: jsonHeaders(token("alice")), + }); + expect(kept.status).toBe(400); + + // The version number is used up. + const again = await publish({ name: "shrinking", version: "2.0.0" }); + expect(again.status).toBe(403); + expect((await publish({ name: "shrinking", version: "2.0.1" })).status).toBe(200); + }); + + test("a change that the registry refuses changes nothing", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + await publish({ name: "steady", version: "1.0.0" }); + await publish({ name: "steady", version: "1.1.0" }, { tag: "next" }); + const before = await packument("steady"); + const without = () => { + const document = structuredClone(before); + delete document.versions["1.1.0"]; + delete document["dist-tags"].next; + return document; + }; + + expect(await put(`steady/-rev/${before._rev}`, { ...without(), "dist-tags": { "1.x": "1.0.0" } })).toMatchObject({ + status: 400, + json: { error: `Bad Request: "1.x" is not a valid dist-tag` }, + }); + expect(await put(`steady/-rev/${before._rev}`, { ...without(), maintainers: [] })).toMatchObject({ + status: 400, + json: { error: "Bad Request: a package needs one maintainer" }, + }); + + expect((await request(`${registry.url}steady/1.1.0`)).status).toBe(200); + expect((await request(`${registry.url}-/package/steady/dist-tags`)).json).toEqual(before["dist-tags"]); + // The same request without the error does the change, so the revision did not move either. + expect((await put(`steady/-rev/${before._rev}`, without())).status).toBe(200); + expect(Object.keys((await packument("steady")).versions)).toEqual(["1.0.0"]); + }); + + test("the whole package", async () => { + const { registry, publish, packument, token } = setup(); + using _ = registry; + await publish({ name: "leaving", version: "1.0.0" }); + const { _rev } = await packument("leaving"); + const unpublish = (user: string) => + request(`${registry.url}leaving/-rev/${_rev}`, { method: "DELETE", headers: jsonHeaders(token(user)) }); + + expect((await unpublish("bob")).status).toBe(403); + expect((await unpublish("alice")).status).toBe(200); + expect((await request(`${registry.url}leaving`)).status).toBe(404); + expect((await request(`${registry.url}leaving/-/leaving-1.0.0.tgz`)).status).toBe(404); + expect((await unpublish("alice")).status).toBe(404); + + const soon = await publish({ name: "leaving", version: "1.0.1" }); + expect(soon.status).toBe(403); + expect(soon.json).toEqual({ error: "leaving cannot be republished until 24 hours have passed." }); + }); + + test("the last version takes the package with it", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + await publish({ name: "single", version: "1.0.0" }); + const document = await packument("single"); + document.versions = {}; + document["dist-tags"] = {}; + expect((await put(`single/-rev/${document._rev}`, document)).status).toBe(200); + expect((await request(`${registry.url}single`)).status).toBe(404); + }); + + test("owners", async () => { + const { registry, publish, packument, put } = setup(); + using _ = registry; + await publish({ name: "shared", version: "1.0.0" }); + const { _id, _rev } = await packument("shared"); + const maintainers = [ + { name: "alice", email: "alice@example.com" }, + { name: "bob", email: "bob@example.com" }, + ]; + expect((await put(`shared/-rev/${_rev}`, { _id, _rev, maintainers: [] })).status).toBe(400); + expect((await put(`shared/-rev/${_rev}`, { _id, _rev, maintainers })).status).toBe(200); + expect((await packument("shared")).maintainers).toEqual(maintainers); + expect((await publish({ name: "shared", version: "1.0.1" }, { user: "bob" })).status).toBe(200); + }); +}); + +describe("advisories", () => { + test("the bulk endpoint answers for the versions the client has", async () => { + const { registry } = setup(); + using _ = registry; + registry.advisories.add("on-disk", { + vulnerable_versions: "<1.1.0", + severity: "critical", + // A field that is there without a value keeps its default. + url: undefined, + title: undefined, + }); + registry.advisories.add("on-disk", { vulnerable_versions: ">=5", title: "not installed" }); + registry.advisories.add("elsewhere", { vulnerable_versions: "*", id: 7, url: "https://example.com/advisory/7" }); + + const ask = (body: unknown, gzip = false) => + request(`${registry.url}-/npm/v1/security/advisories/bulk`, { + method: "POST", + headers: { "content-type": "application/json", ...(gzip ? { "content-encoding": "gzip" } : {}) }, + body: gzip ? Bun.gzipSync(JSON.stringify(body)) : JSON.stringify(body), + }); + + const found = await ask({ "on-disk": ["1.0.0", "1.1.0"], "elsewhere": ["3.0.0"], "unknown": ["1.0.0"] }, true); + expect(found.status).toBe(200); + expect(found.json).toEqual({ + "on-disk": [ + { + id: 1000000, + url: "https://github.com/advisories/GHSA-1000000", + title: "Vulnerability in on-disk", + severity: "critical", + vulnerable_versions: "<1.1.0", + cwe: [], + cvss: { score: 0, vectorString: null }, + }, + ], + "elsewhere": [ + { + id: 7, + url: "https://example.com/advisory/7", + title: "Vulnerability in elsewhere", + severity: "high", + vulnerable_versions: "*", + cwe: [], + cvss: { score: 0, vectorString: null }, + }, + ], + }); + expect(await ask({ "on-disk": ["1.1.0"] })).toMatchObject({ status: 200, text: "{}" }); + expect((await ask([])).status).toBe(400); + }); +}); diff --git a/test/packages/registry/test/read.test.ts b/test/packages/registry/test/read.test.ts new file mode 100644 index 000000000000..7d105f279606 --- /dev/null +++ b/test/packages/registry/test/read.test.ts @@ -0,0 +1,805 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { isIPv6 } from "harness"; +import { join } from "node:path"; +import { Registry } from "../index.ts"; +import { urlHost } from "../src/registry.ts"; +import { abbreviatedAccept, jsonHeaders, md5, request, sha1, sha512, storage } from "./helpers.ts"; + +let fixtures: Awaited>; +let registry: Registry; +let origin: string; + +beforeAll(async () => { + fixtures = await storage([ + { + manifests: [ + { name: "plain", version: "1.0.0" }, + { name: "plain", version: "1.1.0", dependencies: { "@scope/scoped": "^1.0.0" } }, + { name: "plain", version: "2.0.0-beta.1" }, + ], + tags: { latest: "1.1.0", beta: "2.0.0-beta.1" }, + }, + { manifests: [{ name: "@scope/scoped", version: "1.0.0" }] }, + { + manifests: [ + { + name: "everything", + version: "1.0.0", + description: "not in the abbreviated form", + main: "index.js", + scripts: { postinstall: "node build.js", test: "bun test" }, + dependencies: { a: "1" }, + devDependencies: { b: "2" }, + optionalDependencies: { c: "3" }, + peerDependencies: { d: "4" }, + peerDependenciesMeta: { d: { optional: true } }, + bundledDependencies: ["a"], + bin: { everything: "cli.js" }, + directories: { bin: "bin" }, + engines: { bun: ">=1" }, + funding: "https://example.com/fund", + os: ["linux"], + cpu: ["x64"], + libc: ["glibc"], + deprecated: "use something else", + _hasShrinkwrap: false, + }, + { name: "everything", version: "1.0.1", scripts: { test: "bun test" }, _hasShrinkwrap: true }, + { name: "everything", version: "1.0.2", scripts: { install: "" } }, + ], + }, + { manifests: [{ name: "@private/hidden", version: "1.0.0" }] }, + { manifests: [{ name: "untimed", version: "1.0.0" }], time: false }, + { manifests: [{ name: "gone", version: "1.0.0" }], withoutTarball: ["1.0.0"] }, + ]); + registry = new Registry({ storage: fixtures.path, access: { "@private/*": { read: "authenticated" } } }).start(); + origin = registry.url.slice(0, -1); +}); + +afterAll(() => { + // stop() throws when a handler failed. The directory goes away then too. + using _ = fixtures.directory; + registry.stop(); +}); + +describe("packument", () => { + test("abbreviated form", async () => { + const reply = await request(`${origin}/plain`, { headers: { accept: abbreviatedAccept } }); + expect(reply.status).toBe(200); + expect(reply.json).toEqual({ + name: "plain", + "dist-tags": { latest: "1.1.0", beta: "2.0.0-beta.1" }, + versions: { + "1.0.0": { + name: "plain", + version: "1.0.0", + dist: { + integrity: sha512(fixtures.tarballs.get("plain@1.0.0")!), + shasum: sha1(fixtures.tarballs.get("plain@1.0.0")!), + tarball: `${origin}/plain/-/plain-1.0.0.tgz`, + }, + }, + "1.1.0": { + name: "plain", + version: "1.1.0", + dependencies: { "@scope/scoped": "^1.0.0" }, + dist: { + integrity: sha512(fixtures.tarballs.get("plain@1.1.0")!), + shasum: sha1(fixtures.tarballs.get("plain@1.1.0")!), + tarball: `${origin}/plain/-/plain-1.1.0.tgz`, + }, + }, + "2.0.0-beta.1": { + name: "plain", + version: "2.0.0-beta.1", + dist: { + integrity: sha512(fixtures.tarballs.get("plain@2.0.0-beta.1")!), + shasum: sha1(fixtures.tarballs.get("plain@2.0.0-beta.1")!), + tarball: `${origin}/plain/-/plain-2.0.0-beta.1.tgz`, + }, + }, + }, + modified: "2024-03-02T10:20:30.400Z", + }); + expect(reply.headers).toEqual({ + "content-type": "application/vnd.npm.install-v1+json", + "content-length": String(Buffer.byteLength(reply.text)), + "accept-ranges": "bytes", + "cache-control": "public, max-age=300", + // One second is the resolution. The header is never older than `modified`. + "last-modified": "Sat, 02 Mar 2024 10:20:31 GMT", + etag: `"${md5(reply.text)}"`, + vary: "accept-encoding, accept", + }); + // The registry sends compact JSON with the keys in this order. + expect(reply.text.startsWith(`{"name":"plain","dist-tags":{`)).toBe(true); + }); + + test("full form", async () => { + const reply = await request(`${origin}/plain`, { headers: { accept: "application/json" } }); + expect(reply.status).toBe(200); + expect(reply.headers["content-type"]).toBe("application/json"); + expect(Object.keys(reply.json).sort()).toEqual(["_id", "_rev", "dist-tags", "name", "time", "versions"]); + expect(reply.json.time).toEqual({ + created: "2024-01-01T00:00:00.000Z", + modified: "2024-03-02T10:20:30.400Z", + "1.0.0": "2024-01-01T00:00:00.000Z", + "1.1.0": "2024-01-02T00:00:00.000Z", + "2.0.0-beta.1": "2024-01-03T00:00:00.000Z", + }); + expect(reply.json.versions["1.0.0"]).toEqual({ + name: "plain", + version: "1.0.0", + _id: "plain@1.0.0", + dist: { + integrity: sha512(fixtures.tarballs.get("plain@1.0.0")!), + shasum: sha1(fixtures.tarballs.get("plain@1.0.0")!), + tarball: `${origin}/plain/-/plain-1.0.0.tgz`, + }, + }); + }); + + test("the abbreviated version object is an allow list", async () => { + const reply = await request(`${origin}/everything`, { headers: { accept: abbreviatedAccept } }); + const { dist, ...version } = reply.json.versions["1.0.0"]; + expect(version).toEqual({ + name: "everything", + version: "1.0.0", + deprecated: "use something else", + dependencies: { a: "1" }, + optionalDependencies: { c: "3" }, + devDependencies: { b: "2" }, + bundleDependencies: ["a"], + peerDependencies: { d: "4" }, + peerDependenciesMeta: { d: { optional: true } }, + bin: { everything: "cli.js" }, + directories: { bin: "bin" }, + engines: { bun: ">=1" }, + funding: "https://example.com/fund", + cpu: ["x64"], + os: ["linux"], + hasInstallScript: true, + }); + const { dist: _, ...second } = reply.json.versions["1.0.1"]; + expect(second).toEqual({ name: "everything", version: "1.0.1", _hasShrinkwrap: true }); + // An install script that is empty does not count. + expect(reply.json.versions["1.0.2"]).not.toHaveProperty("hasInstallScript"); + }); + + test.each([ + ["application/vnd.npm.install-v1+json", true], + [abbreviatedAccept, true], + ["application/json, application/vnd.npm.install-v1+json", true], + // The registry looks for the media type. It does not weigh q. + ["application/json; q=1.0, application/vnd.npm.install-v1+json; q=0.1", true], + ["application/vnd.npm.install-v1+json; charset=utf-8", true], + ["APPLICATION/VND.NPM.INSTALL-V1+JSON", false], + ["application/vnd.npm.install-v2+json", false], + ["application/json, */*", false], + ["application/*", false], + ["*/*", false], + ["text/html", false], + ])("Accept: %s", async (accept, abbreviated) => { + const reply = await request(`${origin}/plain`, { headers: { accept } }); + expect(reply.headers["content-type"]).toBe( + abbreviated ? "application/vnd.npm.install-v1+json" : "application/json", + ); + expect("time" in reply.json).toBe(!abbreviated); + }); + + test.each(["/@scope%2fscoped", "/@scope%2Fscoped", "/@scope/scoped", "/%40scope%2fscoped", "/@scope/scoped/"])( + "a scoped name as %s", + async path => { + const reply = await request(origin + path, { headers: { accept: abbreviatedAccept } }); + expect(reply.status).toBe(200); + expect(reply.json.name).toBe("@scope/scoped"); + expect(reply.json.versions["1.0.0"].dist.tarball).toBe(`${origin}/@scope/scoped/-/scoped-1.0.0.tgz`); + }, + ); + + test("the tarball URL follows the host of the request", async () => { + const byAddress = `http://127.0.0.1:${registry.port}`; + const reply = await request(`${byAddress}/plain`, { headers: { accept: abbreviatedAccept } }); + expect(reply.json.versions["1.0.0"].dist.tarball).toBe(`${byAddress}/plain/-/plain-1.0.0.tgz`); + }); + + test("publicUrl fixes the tarball URL", async () => { + using fixed = new Registry({ storage: fixtures.path, publicUrl: "https://registry.example.com/" }).start(); + const reply = await request(`${fixed.url}plain`); + expect(reply.json.versions["1.0.0"].dist.tarball).toBe("https://registry.example.com/plain/-/plain-1.0.0.tgz"); + }); + + test("a packument without time gets modified from the file", async () => { + const reply = await request(`${origin}/untimed`, { headers: { accept: abbreviatedAccept } }); + expect(new Date(reply.json.modified).toISOString()).toBe(reply.json.modified); + const full = await request(`${origin}/untimed`); + expect(full.json).not.toHaveProperty("time"); + }); + + test.each([ + ["/nothing-here", "an unknown name"], + ["/@scope%2fnothing-here", "an unknown scoped name"], + // These two are the directory of "plain" to a file system that ignores case, or a period at the end. + ["/Plain", "another case"], + ["/plain.", "a period at the end"], + ["/.hidden", "a name that starts with a period"], + ["/has%20space", "a name that is not URL-safe"], + ["/..%2f..%2fetc%2fpasswd", "a path that leaves the storage"], + ["/@scope", "a scope without a name"], + ["/%E0%A4%A", "a malformed escape"], + ])("404 for %s (%s)", async path => { + const reply = await request(origin + path, { headers: { accept: abbreviatedAccept } }); + expect({ status: reply.status, body: reply.text, type: reply.headers["content-type"] }).toEqual({ + status: 404, + body: `{"error":"Not found"}`, + type: "application/json", + }); + }); + + test("?write=true is the document that a client edits", async () => { + const full = await request(`${origin}/plain`); + const reply = await request(`${origin}/plain?write=true`, { headers: { accept: abbreviatedAccept } }); + expect(reply.status).toBe(200); + // The full document, whatever the Accept header asks for, and nothing to revalidate it with. + expect(reply.json).toEqual(full.json); + expect(reply.headers).toEqual({ + "content-type": "application/json", + "content-length": String(Buffer.byteLength(full.text)), + "cache-control": "public, max-age=300", + vary: "accept-encoding, accept", + }); + const conditional = await request(`${origin}/plain?write=true`, { + headers: { "if-none-match": full.headers.etag }, + }); + expect(conditional.status).toBe(200); + }); + + test("HEAD", async () => { + const get = await request(`${origin}/plain`); + const head = await request(`${origin}/plain`, { method: "HEAD" }); + expect(head.status).toBe(200); + expect(head.text).toBe(""); + expect(head.headers).toEqual(get.headers); + }); + + test("other methods", async () => { + const reply = await request(`${origin}/plain`, { method: "POST", body: "{}" }); + expect(reply.status).toBe(405); + expect(reply.headers.allow).toBe("GET, HEAD, PUT"); + expect(reply.json).toEqual({ code: "MethodNotAllowedError", message: "POST is not allowed" }); + }); +}); + +describe("conditional requests and encodings", () => { + test("If-None-Match", async () => { + const first = await request(`${origin}/plain`, { headers: { accept: abbreviatedAccept } }); + const etag = first.headers.etag; + for (const validator of [etag, `W/${etag}`, `"other", ${etag}`, "*"]) { + const reply = await request(`${origin}/plain`, { + headers: { accept: abbreviatedAccept, "if-none-match": validator }, + }); + expect(reply.status).toBe(304); + expect(reply.text).toBe(""); + expect(reply.headers).toMatchObject({ + "cache-control": "public, max-age=300", + "last-modified": "Sat, 02 Mar 2024 10:20:31 GMT", + etag, + }); + } + + // What the registry itself answers, without the HTTP server between. A 304 has the validators and nothing + // that describes a body. The test above cannot be this exact: the server can add headers of its own. + const direct = await registry.fetch( + new Request(`${origin}/plain`, { headers: { accept: abbreviatedAccept, "if-none-match": etag } }), + ); + expect(direct.status).toBe(304); + expect(Object.fromEntries(direct.headers)).toEqual({ + "cache-control": "public, max-age=300", + "last-modified": "Sat, 02 Mar 2024 10:20:31 GMT", + etag, + }); + const changed = await request(`${origin}/plain`, { + headers: { accept: abbreviatedAccept, "if-none-match": `"0123456789abcdef0123456789abcdef"` }, + }); + expect(changed.status).toBe(200); + expect(changed.text).toBe(first.text); + }); + + test("the two forms have different entity tags", async () => { + const abbreviated = await request(`${origin}/plain`, { headers: { accept: abbreviatedAccept } }); + const reply = await request(`${origin}/plain`, { headers: { "if-none-match": abbreviated.headers.etag } }); + expect(reply.status).toBe(200); + }); + + test("If-Modified-Since has no effect", async () => { + const reply = await request(`${origin}/plain`, { + headers: { accept: abbreviatedAccept, "if-modified-since": "Sat, 02 Mar 2024 10:20:31 GMT" }, + }); + expect(reply.status).toBe(200); + }); + + test.each([ + ["gzip, deflate, br, zstd", "br"], + ["br", "br"], + ["gzip", "gzip"], + ["gzip, deflate", "gzip"], + ["br;q=0, gzip", "gzip"], + ["zstd", "identity"], + ["deflate", "identity"], + ["identity", "identity"], + ])("Accept-Encoding: %s", async (accepted, used) => { + const plain = await request(`${origin}/plain`, { headers: { accept: abbreviatedAccept } }); + const reply = await request(`${origin}/plain`, { + headers: { accept: abbreviatedAccept, "accept-encoding": accepted }, + }); + expect(reply.headers["content-encoding"] ?? "identity").toBe(used); + expect(reply.text).toBe(plain.text); + // An encoded answer is a different representation, so its entity tag is weak. + expect(reply.headers.etag).toBe(used === "identity" ? plain.headers.etag : `W/${plain.headers.etag}`); + }); + + test("Range", async () => { + const whole = await request(`${origin}/plain`, { headers: { accept: abbreviatedAccept } }); + const size = Buffer.byteLength(whole.text); + // A part is never encoded, also for a client that accepts an encoding. + for (const encoding of ["identity", "gzip, br"]) { + const part = await request(`${origin}/plain`, { + headers: { accept: abbreviatedAccept, range: "bytes=0-9", "accept-encoding": encoding }, + }); + expect(part.status).toBe(206); + expect(part.text).toBe(whole.text.slice(0, 10)); + expect(part.headers).toEqual({ + "content-type": "application/vnd.npm.install-v1+json", + "content-length": "10", + "content-range": `bytes 0-9/${size}`, + "cache-control": "public, max-age=300", + "last-modified": "Sat, 02 Mar 2024 10:20:31 GMT", + etag: whole.headers.etag, + vary: "accept-encoding, accept", + }); + } + const past = await request(`${origin}/plain`, { + headers: { accept: abbreviatedAccept, range: `bytes=${size}-` }, + }); + expect(past.status).toBe(416); + expect(past.text).toBe(`{"error":"Requested range not satisfiable"}`); + expect(past.headers).toEqual({ + "content-type": "application/json", + "content-length": "43", + "content-range": `bytes */${size}`, + "last-modified": "Sat, 02 Mar 2024 10:20:31 GMT", + etag: whole.headers.etag, + }); + }); + + test("a short body is not encoded", async () => { + const reply = await request(`${origin}/nothing-here`, { headers: { "accept-encoding": "gzip, br" } }); + expect(reply.headers).toEqual({ "content-type": "application/json", "content-length": "21" }); + }); +}); + +describe("version", () => { + test.each([ + ["/plain/1.0.0", "1.0.0"], + ["/plain/latest", "1.1.0"], + ["/plain/beta", "2.0.0-beta.1"], + ["/@scope%2fscoped/latest", "1.0.0"], + ["/@scope/scoped/1.0.0", "1.0.0"], + ])("%s", async (path, version) => { + // The answer is the full version object, whatever the Accept header asks for. + const reply = await request(origin + path, { headers: { accept: abbreviatedAccept } }); + expect(reply.status).toBe(200); + expect(reply.headers["content-type"]).toBe("application/json"); + expect(reply.headers["cache-control"]).toBe("max-age=300"); + expect(reply.headers).not.toHaveProperty("etag"); + expect(reply.json.version).toBe(version); + expect(reply.json._id).toBe(`${reply.json.name}@${version}`); + expect(reply.json.dist.tarball).toStartWith(`${origin}/${reply.json.name}/-/`); + }); + + test.each(["9.9.9", "^1.0.0", "v1.0.0", "nope", "constructor", "__proto__"])("%s is not there", async wanted => { + const reply = await request(`${origin}/plain/${encodeURIComponent(wanted)}`); + expect(reply.status).toBe(404); + // The body is a JSON string, not an object. + expect(reply.text).toBe(JSON.stringify(`version not found: ${wanted}`)); + }); +}); + +describe("tarball", () => { + test("GET", async () => { + const tarball = fixtures.tarballs.get("plain@1.0.0")!; + const reply = await request(`${origin}/plain/-/plain-1.0.0.tgz`, { headers: { "accept-encoding": "gzip, br" } }); + expect(reply.status).toBe(200); + expect(Buffer.from(reply.bytes).equals(tarball)).toBe(true); + expect(reply.headers).toEqual({ + "content-type": "application/octet-stream", + "content-length": String(tarball.byteLength), + "accept-ranges": "bytes", + "cache-control": "public, immutable, max-age=31557600", + etag: `"${md5(tarball)}"`, + "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", + }); + }); + + test.each(["/@scope/scoped/-/scoped-1.0.0.tgz", "/@scope%2fscoped/-/scoped-1.0.0.tgz"])("%s", async path => { + const reply = await request(origin + path); + expect(reply.status).toBe(200); + expect(sha512(reply.bytes)).toBe(sha512(fixtures.tarballs.get("@scope/scoped@1.0.0")!)); + }); + + test("the scope is not part of the file name", async () => { + const reply = await request(`${origin}/@scope/scoped/-/@scope/scoped-1.0.0.tgz`); + expect(reply.status).toBe(404); + expect(reply.json).toEqual({ + code: "ResourceNotFound", + message: "/@scope/scoped/-/@scope/scoped-1.0.0.tgz does not exist", + }); + }); + + test.each([ + "/plain/-/plain-9.9.9.tgz", + "/plain/-/other-1.0.0.tgz", + "/plain/-/package.json", + "/plain/-/..%2f..%2fplain%2fpackage.json", + "/gone/-/gone-1.0.0.tgz", + "/nothing-here/-/nothing-here-1.0.0.tgz", + ])("404 for %s", async path => { + const reply = await request(origin + path); + expect({ status: reply.status, body: reply.text }).toEqual({ status: 404, body: `{"error":"Not found"}` }); + }); + + test("HEAD and If-None-Match", async () => { + const tarball = fixtures.tarballs.get("plain@1.0.0")!; + const head = await request(`${origin}/plain/-/plain-1.0.0.tgz`, { method: "HEAD" }); + expect(head.status).toBe(200); + expect(head.text).toBe(""); + expect(head.headers["content-length"]).toBe(String(tarball.byteLength)); + + const unchanged = await request(`${origin}/plain/-/plain-1.0.0.tgz`, { + headers: { "if-none-match": head.headers.etag }, + }); + expect(unchanged.status).toBe(304); + expect(unchanged.text).toBe(""); + expect(unchanged.headers).toMatchObject({ + "cache-control": "public, max-age=300", + etag: head.headers.etag, + "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", + }); + + const direct = await registry.fetch( + new Request(`${origin}/plain/-/plain-1.0.0.tgz`, { headers: { "if-none-match": head.headers.etag } }), + ); + expect(direct.status).toBe(304); + expect(Object.fromEntries(direct.headers)).toEqual({ + "cache-control": "public, max-age=300", + etag: head.headers.etag, + "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", + }); + }); + + test("Range", async () => { + const tarball = fixtures.tarballs.get("plain@1.0.0")!; + const size = tarball.byteLength; + for (const [range, start, end] of [ + ["bytes=0-9", 0, 9], + ["bytes=10-", 10, size - 1], + ["bytes=-10", size - 10, size - 1], + [`bytes=5-${size + 100}`, 5, size - 1], + ] as const) { + const reply = await request(`${origin}/plain/-/plain-1.0.0.tgz`, { headers: { range } }); + expect(reply.status).toBe(206); + expect(reply.headers["content-range"]).toBe(`bytes ${start}-${end}/${size}`); + expect(reply.headers).not.toHaveProperty("accept-ranges"); + expect(Buffer.from(reply.bytes).equals(tarball.subarray(start, end + 1))).toBe(true); + } + const past = await request(`${origin}/plain/-/plain-1.0.0.tgz`, { headers: { range: `bytes=${size}-` } }); + expect(past.status).toBe(416); + expect(past.headers["content-range"]).toBe(`bytes */${size}`); + expect(past.text).toBe(`{"error":"Requested range not satisfiable"}`); + }); +}); + +describe("restricted packages", () => { + test("look like they are not there", async () => { + for (const path of ["/@private%2fhidden", "/@private/hidden/1.0.0", "/@private/hidden/-/hidden-1.0.0.tgz"]) { + const unknown: Record[] = [ + {}, + { authorization: "Bearer npm_notATokenOfThisRegistry000000000000" }, + ]; + for (const headers of unknown) { + const reply = await request(origin + path, { headers }); + expect({ path, status: reply.status, body: reply.text }).toEqual({ + path, + status: 404, + body: `{"error":"Not found"}`, + }); + } + } + const tags = await request(`${origin}/-/package/@private%2fhidden/dist-tags`); + expect({ status: tags.status, body: tags.text }).toEqual({ status: 404, body: `"Not Found"` }); + }); + + test("a user reads them with a token or with a password", async () => { + using own = new Registry({ storage: fixtures.path, access: { "@private/*": { read: "authenticated" } } }).start(); + const { token } = own.auth.createToken(own.auth.addUser("reader", "secret")); + for (const authorization of [`Bearer ${token}`, `Basic ${btoa("reader:secret")}`]) { + const packument = await request(`${own.url}@private%2fhidden`, { headers: { authorization } }); + expect(packument.status).toBe(200); + const tarball = await request(packument.json.versions["1.0.0"].dist.tarball, { headers: { authorization } }); + expect(sha512(tarball.bytes)).toBe(packument.json.versions["1.0.0"].dist.integrity); + } + const wrong = await request(`${own.url}@private%2fhidden`, { + headers: { authorization: `Basic ${btoa("reader:wrong")}` }, + }); + expect(wrong.status).toBe(404); + }); + + test("credentials that are wrong do not block a public package", async () => { + for (const authorization of ["Bearer nope", "Basic bm9wZTpub3Bl", "Digest x", "garbage"]) { + const reply = await request(`${origin}/plain`, { headers: { authorization } }); + expect(reply.status).toBe(200); + } + }); +}); + +describe("services", () => { + test("dist-tags", async () => { + for (const path of ["/-/package/plain/dist-tags", "/-/package/@scope%2fscoped/dist-tags"]) { + const reply = await request(origin + path); + expect(reply.status).toBe(200); + expect(reply.headers["content-type"]).toBe("application/json"); + } + expect((await request(`${origin}/-/package/plain/dist-tags`)).json).toEqual({ + latest: "1.1.0", + beta: "2.0.0-beta.1", + }); + expect((await request(`${origin}/-/package/@scope/scoped/dist-tags`)).json).toEqual({ latest: "1.0.0" }); + const missing = await request(`${origin}/-/package/nothing-here/dist-tags`); + expect({ status: missing.status, body: missing.text }).toEqual({ status: 404, body: `"Not Found"` }); + }); + + test("ping, root and keys", async () => { + expect(await request(`${origin}/-/ping`)).toMatchObject({ status: 200, text: "{}" }); + expect(await request(`${origin}/-/ping?write=true`)).toMatchObject({ status: 200, text: "{}" }); + expect(await request(`${origin}/`)).toMatchObject({ status: 200, text: "{}" }); + expect(await request(`${origin}/-/npm/v1/keys`)).toMatchObject({ status: 200, json: { keys: [] } }); + }); + + test("a path that no service owns", async () => { + expect(await request(`${origin}/-/all`)).toMatchObject({ + status: 404, + json: { code: "ResourceNotFound", message: "/-/all does not exist" }, + }); + const bulk = await request(`${origin}/-/npm/v1/security/advisories/bulk`); + expect(bulk.status).toBe(405); + expect(bulk.headers.allow).toBe("POST"); + expect(bulk.json).toEqual({ code: "MethodNotAllowedError", message: "GET is not allowed" }); + }); + + test("visibility and collaborators", async () => { + expect((await request(`${origin}/-/package/plain/visibility`)).json).toEqual({ public: true }); + expect((await request(`${origin}/-/package/plain/collaborators`)).json).toEqual({}); + + // A package that is not there is not public, and that is a 200. + for (const name of ["nothing-here", "@private%2fhidden"]) { + expect(await request(`${origin}/-/package/${name}/visibility`)).toMatchObject({ + status: 200, + text: `{"public":false}`, + }); + expect(await request(`${origin}/-/package/${name}/collaborators`)).toMatchObject({ + status: 404, + text: `{"error":"Package not found"}`, + }); + } + + // A packument ignores credentials that are wrong. This endpoint does not. + const unknown = await request(`${origin}/-/package/plain/collaborators`, { + headers: { authorization: "Bearer npm_notATokenOfThisRegistry000000000000" }, + }); + expect(unknown).toMatchObject({ + status: 401, + text: `{"error":"You must be logged in to publish packages."}`, + headers: { "www-authenticate": "Basic, Bearer" }, + }); + }); + + test("search", async () => { + const all = await request(`${origin}/-/v1/search`); + expect(all.json.total).toBe(5); + expect(all.json.objects.map((found: any) => found.package.name)).toEqual([ + "@scope/scoped", + "everything", + "gone", + "plain", + "untimed", + ]); + + const one = await request(`${origin}/-/v1/search?text=plain&size=1`); + expect(one.json.objects).toHaveLength(1); + expect(one.json.objects[0].package).toEqual({ + name: "plain", + scope: "unscoped", + version: "1.1.0", + keywords: [], + date: "2024-01-02T00:00:00.000Z", + links: { npm: `${origin}/plain` }, + maintainers: [], + }); + expect(Object.keys(one.json).sort()).toEqual(["objects", "time", "total"]); + expect(Object.keys(one.json.objects[0]).sort()).toEqual(["package", "score", "searchScore"]); + + const scoped = await request(`${origin}/-/v1/search?text=scope:scope`); + expect(scoped.json.objects.map((found: any) => found.package.name)).toEqual(["@scope/scoped"]); + expect((await request(`${origin}/-/v1/search?text=zzz`)).json.total).toBe(0); + + // A package is found when each word matches, in every order of the words. + const found = async (text: string) => { + const reply = await request(`${origin}/-/v1/search?text=${encodeURIComponent(text)}`); + return reply.json.objects.map((found: any) => found.package.name); + }; + expect({ + "zzz scope:scope": await found("zzz scope:scope"), + "scope:scope zzz": await found("scope:scope zzz"), + "scoped scope:scope": await found("scoped scope:scope"), + "scope:scope scoped": await found("scope:scope scoped"), + "scope:other scoped": await found("scope:other scoped"), + }).toEqual({ + "zzz scope:scope": [], + "scope:scope zzz": [], + "scoped scope:scope": ["@scope/scoped"], + "scope:scope scoped": ["@scope/scoped"], + "scope:other scoped": [], + }); + expect((await request(`${origin}/-/v1/search?from=4`)).json.objects).toHaveLength(1); + }); +}); + +describe("hooks", () => { + test("recordRequests and intercept", async () => { + using own = new Registry({ + storage: fixtures.path, + recordRequests: true, + intercept: request => { + if (new URL(request.url).pathname === "/plain/-/plain-1.0.0.tgz") { + return new Response("out of order", { status: 503 }); + } + }, + }).start(); + await request(`${own.url}plain?write=true`, { headers: { accept: abbreviatedAccept } }); + await request(`${own.url}plain/-/plain-1.0.0.tgz`); + await request(`${own.url}plain/-/plain-1.1.0.tgz`); + expect(own.requests.map(({ method, path, status }) => ({ method, path, status }))).toEqual([ + { method: "GET", path: "/plain?write=true", status: 200 }, + { method: "GET", path: "/plain/-/plain-1.0.0.tgz", status: 503 }, + { method: "GET", path: "/plain/-/plain-1.1.0.tgz", status: 200 }, + ]); + expect(own.requests[0].headers.accept).toBe(abbreviatedAccept); + }); + + test("intercept reads the body of a request that the registry answers", async () => { + const bodies: unknown[] = []; + using own = new Registry({ + intercept: async request => { + if (request.method === "PUT") bodies.push(await request.json()); + }, + }).start(); + const user = { name: "reader", password: "secret", email: "reader@example.com" }; + const reply = await request(`${own.url}-/user/org.couchdb.user:reader`, { + method: "PUT", + headers: jsonHeaders(), + body: JSON.stringify(user), + }); + expect(reply).toMatchObject({ status: 201, json: { ok: true, id: "org.couchdb.user:reader" } }); + expect(bodies).toEqual([user]); + }); + + test("an error in intercept fails the test that stops the registry", async () => { + using own = new Registry({ + storage: fixtures.path, + intercept: request => { + // What a failed expect() in a handler does. + if (new URL(request.url).pathname === "/plain") throw new Error("the handler did not expect /plain"); + }, + }).start(); + // The client gets a 500, which a test that only looks at the exit code of bun can miss. + const reply = await request(`${own.url}plain`); + expect({ status: reply.status, text: reply.text }).toEqual({ + status: 500, + text: `{"error":"Internal Server Error"}`, + }); + expect((await request(`${own.url}everything`)).status).toBe(200); + expect(() => own.stop()).toThrow("the handler did not expect /plain"); + // The error is reported once. + expect(() => own.stop()).not.toThrow(); + }); + + test("an answer of the registry is not an error", async () => { + using own = new Registry({ storage: fixtures.path }).start(); + expect((await request(`${own.url}nothing-here`)).status).toBe(404); + expect((await request(`${own.url}-/whoami`)).status).toBe(401); + expect(() => own.stop()).not.toThrow(); + }); + + test.each([ + [undefined, "localhost"], + ["127.0.0.1", "localhost"], + ["localhost", "localhost"], + // A registry that listens on every address is there on the loopback too. + ["0.0.0.0", "localhost"], + ["::", "localhost"], + ["::1", "[::1]"], + ["fe80::1", "[fe80::1]"], + ["192.168.1.10", "192.168.1.10"], + ["registry.test", "registry.test"], + ])("the URL of a registry on %p has the host %s", (hostname, host) => { + expect(urlHost(hostname)).toBe(host); + }); + + test.skipIf(!isIPv6())("the URL of a registry on the IPv6 loopback reaches it", async () => { + using own = new Registry({ storage: fixtures.path, hostname: "::1" }).start(); + expect(own.url).toBe(`http://[::1]:${own.port}/`); + const reply = await request(`${own.url}plain`, { headers: { accept: abbreviatedAccept } }); + expect(reply.status).toBe(200); + expect(reply.json.versions["1.0.0"].dist.tarball).toBe(`${own.url}plain/-/plain-1.0.0.tgz`); + }); + + test("a port has one registry", async () => { + using first = new Registry({ storage: fixtures.path }).start(); + using second = new Registry({ storage: fixtures.path, port: first.port }); + expect(() => second.start()).toThrow(expect.objectContaining({ code: "EADDRINUSE" })); + expect(second.listening).toBe(false); + expect((await request(`${first.url}-/ping`)).status).toBe(200); + }); + + test("stop and start keep the state", async () => { + using own = new Registry({ storage: fixtures.path }); + expect(() => own.port).toThrow("The registry has no port before start()"); + own.auth.addUser("keeper", "secret"); + own.start(); + expect(own.start()).toBe(own); + expect((await request(`${own.url}-/ping`)).status).toBe(200); + own.stop(); + expect(own.listening).toBe(false); + own.start(); + const reply = await request(`${own.url}-/whoami`, { + headers: { authorization: `Basic ${btoa("keeper:secret")}` }, + }); + expect(reply.json).toEqual({ username: "keeper" }); + }); +}); + +describe("storage", () => { + test("must be a directory", () => { + const missing = join(fixtures.path, "not-there"); + const file = join(fixtures.path, "plain", "package.json"); + expect(() => new Registry({ storage: missing })).toThrow(`The storage is not a directory: ${missing}`); + expect(() => new Registry({ storage: file })).toThrow(`The storage is not a directory: ${file}`); + }); + + test("a file is not a package and not a scope", async () => { + const files = await storage([{ manifests: [{ name: "real", version: "1.0.0" }] }]); + using _ = files.directory; + await Promise.all([Bun.write(join(files.path, "a-file"), ""), Bun.write(join(files.path, "@a-file"), "")]); + using own = new Registry({ storage: files.path }).start(); + expect({ + package: (await request(`${own.url}a-file`)).status, + scope: (await request(`${own.url}@a-file%2fpackage`)).status, + real: (await request(`${own.url}real`)).status, + }).toEqual({ package: 404, scope: 404, real: 200 }); + }); + + test("a read that failed is not the state of the package", async () => { + const files = await storage([{ manifests: [{ name: "mended", version: "1.0.0" }] }]); + using _ = files.directory; + const packument = join(files.path, "mended", "package.json"); + const valid = await Bun.file(packument).text(); + await Bun.write(packument, "{"); + + using own = new Registry({ storage: files.path }).start(); + expect((await request(`${own.url}mended`)).status).toBe(500); + await Bun.write(packument, valid); + expect((await request(`${own.url}mended`)).status).toBe(200); + // The read that failed is an error of the registry, so stop() reports it. + expect(() => own.stop()).toThrow(`${packument} is not a packument`); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 226f9257beea..3f60f228353f 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -7,6 +7,7 @@ "baseUrl": ".", "paths": { "harness": ["./harness.ts"], + "registry": ["./packages/registry/test-registry.ts"], "mkfifo": ["./mkfifo.ts"], "node-harness": ["./js/node/harness.ts"], "s3-server": ["./packages/s3-server/index.ts"],