diff --git a/package-lock.json b/package-lock.json index bb6bcfa1..26565b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,27 +20,25 @@ "download-chromium": "^2.2.1", "express": "^4.17.1", "form-data": "^4.0.0", + "got": "^12.5.3", + "got-fetch": "^5.1.4", "is-wsl": "^2.2.0", - "lodash": "^4.17.21", "lodash-es": "^4.17.21", "markdown-it": "^12.3.2", "markdown-it-table-of-contents": "^0.6.0", "mime-types": "^2.1.34", - "node-fetch": "3.0.x", - "oidc-client": "^1.11.5", + "node-abort-controller": "^3.1.1", "puppeteer-core": "^13.5.1", "randomstring": "^1.1.4", "react": "^17.0.2", "react-dom": "^17.0.2", - "sanitize-filename": "^1.6.3", - "utility-types": "^3.10.0" + "sanitize-filename": "^1.6.3" }, "devDependencies": { "@cnblogs/eslint-config-typescript": "^1.0.1", "@cnblogs/prettier-config": "^2.0.3", "@types/express": "^4.17.1", "@types/glob": "^7.1.4", - "@types/lodash": "^4.14.178", "@types/lodash-es": "^4.17.6", "@types/markdown-it": "^12.2.3", "@types/mime-types": "^2.1.1", @@ -79,6 +77,7 @@ "ts-loader": "^9.2.5", "tsconfig-paths-webpack-plugin": "^3.5.2", "typescript": "^4.8.4", + "utility-types": "^3.10.0", "webpack": "5.70.x", "webpack-cli": "^4.8.0" }, @@ -778,22 +777,25 @@ } }, "node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", + "integrity": "sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==", "engines": { - "node": ">=6" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, "node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dependencies": { - "defer-to-connect": "^1.0.1" + "defer-to-connect": "^2.0.1" }, "engines": { - "node": ">=6" + "node": ">=14.16" } }, "node_modules/@tootallnate/once": { @@ -883,6 +885,11 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "node_modules/@types/json-schema": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", @@ -1988,29 +1995,40 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "engines": { + "node": ">=14.16" + } + }, "node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "version": "10.2.7", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.7.tgz", + "integrity": "sha512-I4SA6mKgDxcxVbSt/UmIkb9Ny8qSkg6ReBHtAAXnZHk7KOSx5g3DTiAOaYzcHCs6oOdHn+bip9T48E6tMvK9hw==", "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" + "@types/http-cache-semantics": "^4.0.1", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.2", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.16" } }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/callsites": { @@ -2181,11 +2199,22 @@ } }, "node_modules/clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dependencies": { "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" } }, "node_modules/co": { @@ -2396,16 +2425,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/core-js": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", - "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2499,11 +2518,6 @@ "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." }, - "node_modules/crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" - }, "node_modules/css-blank-pseudo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", @@ -2604,14 +2618,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "engines": { - "node": ">= 6" - } - }, "node_modules/date-fns": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", @@ -2653,14 +2659,28 @@ } }, "node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dependencies": { - "mimic-response": "^1.0.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/deep-is": { @@ -2669,9 +2689,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } }, "node_modules/defined": { "version": "1.0.0", @@ -2862,6 +2885,66 @@ "download-chromium": "bin.js" } }, + "node_modules/download-chromium/node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/download-chromium/node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/download-chromium/node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/download-chromium/node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/download-chromium/node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download-chromium/node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "node_modules/download-chromium/node_modules/extract-zip": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", @@ -2884,11 +2967,96 @@ "ms": "2.0.0" } }, + "node_modules/download-chromium/node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/download-chromium/node_modules/got/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/download-chromium/node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" + }, + "node_modules/download-chromium/node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/download-chromium/node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/download-chromium/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/download-chromium/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/download-chromium/node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/download-chromium/node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/download-chromium/node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -2929,9 +3097,9 @@ } }, "node_modules/duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" }, "node_modules/ee-first": { "version": "1.1.1", @@ -3597,28 +3765,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz", - "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3757,6 +3903,14 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "engines": { + "node": ">= 14.17" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4041,35 +4195,49 @@ } }, "node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "version": "12.5.3", + "resolved": "https://registry.npmjs.org/got/-/got-12.5.3.tgz", + "integrity": "sha512-8wKnb9MGU8IPGRIo+/ukTy9XLJBwDiCpIf5TVzQ9Cpol50eMTpBq2GAuDsuDIz7hTYmZgMgC1e9ydr6kSDWs3w==", "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.1", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" }, "engines": { - "node": ">=8.6" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/got/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dependencies": { - "pump": "^3.0.0" + "node_modules/got-fetch": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/got-fetch/-/got-fetch-5.1.4.tgz", + "integrity": "sha512-Pdv+SSgtTCDZbNuGkOf0c0FZy5Syqf225JEgG/vKt13HAe8r2vNJ11DxKKODsmr7L7/6ff3lf+AxpBtD5k/dRQ==", + "engines": { + "node": ">=14.0.0" }, + "peerDependencies": { + "got": "^12.0.0" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graceful-fs": { @@ -4126,9 +4294,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http-errors": { "version": "1.8.1", @@ -4159,6 +4327,18 @@ "node": ">= 6" } }, + "node_modules/http2-wrapper": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", + "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -4578,9 +4758,9 @@ } }, "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", @@ -4616,11 +4796,11 @@ } }, "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", "dependencies": { - "json-buffer": "3.0.0" + "json-buffer": "3.0.1" } }, "node_modules/kind-of": { @@ -4764,11 +4944,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -4808,11 +4983,14 @@ } }, "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lru-cache": { @@ -5032,11 +5210,14 @@ } }, "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "engines": { - "node": ">=4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mini-css-extract-plugin": { @@ -5255,47 +5436,18 @@ "dev": true }, "node_modules/netmask": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", - "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", - "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", - "dependencies": { - "data-uri-to-buffer": "^3.0.1", - "fetch-blob": "^3.1.2" - }, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "node": ">= 0.4.0" } }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, "node_modules/node-releases": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", @@ -5321,11 +5473,14 @@ } }, "node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", + "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/npm": { @@ -7732,45 +7887,6 @@ "node": ">= 6" } }, - "node_modules/oidc-client": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", - "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", - "dependencies": { - "acorn": "^7.4.1", - "base64-js": "^1.5.1", - "core-js": "^3.8.3", - "crypto-js": "^4.0.0", - "serialize-javascript": "^4.0.0" - } - }, - "node_modules/oidc-client/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/oidc-client/node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/oidc-client/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -7823,11 +7939,11 @@ } }, "node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "engines": { - "node": ">=6" + "node": ">=12.20" } }, "node_modules/p-limit": { @@ -8880,7 +8996,7 @@ "node_modules/prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "engines": { "node": ">=4" } @@ -9132,7 +9248,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, "engines": { "node": ">=10" }, @@ -9297,6 +9412,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -9328,11 +9448,17 @@ } }, "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dependencies": { - "lowercase-keys": "^1.0.0" + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/reusify": { @@ -10237,7 +10363,7 @@ "node_modules/url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dependencies": { "prepend-http": "^2.0.0" }, @@ -10259,6 +10385,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "dev": true, "engines": { "node": ">= 4" } @@ -10298,14 +10425,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", - "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -11221,16 +11340,16 @@ } }, "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", + "integrity": "sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==" }, "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "requires": { - "defer-to-connect": "^1.0.1" + "defer-to-connect": "^2.0.1" } }, "@tootallnate/once": { @@ -11317,6 +11436,11 @@ "@types/node": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "@types/json-schema": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", @@ -12159,24 +12283,29 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==" + }, "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "version": "10.2.7", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.7.tgz", + "integrity": "sha512-I4SA6mKgDxcxVbSt/UmIkb9Ny8qSkg6ReBHtAAXnZHk7KOSx5g3DTiAOaYzcHCs6oOdHn+bip9T48E6tMvK9hw==", "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" + "@types/http-cache-semantics": "^4.0.1", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.2", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" }, "dependencies": { - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" } } }, @@ -12293,11 +12422,18 @@ } }, "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "requires": { "mimic-response": "^1.0.0" + }, + "dependencies": { + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + } } }, "co": { @@ -12459,11 +12595,6 @@ } } }, - "core-js": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", - "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==" - }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -12537,11 +12668,6 @@ "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" }, - "crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" - }, "css-blank-pseudo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", @@ -12600,11 +12726,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, - "data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" - }, "date-fns": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", @@ -12625,11 +12746,18 @@ "dev": true }, "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "requires": { - "mimic-response": "^1.0.0" + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } } }, "deep-is": { @@ -12638,9 +12766,9 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" }, "defined": { "version": "1.0.0", @@ -12796,6 +12924,53 @@ "proxy-from-env": "^1.0.0" }, "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "extract-zip": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", @@ -12817,10 +12992,79 @@ } } }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "requires": { + "lowercase-keys": "^1.0.0" + } } } }, @@ -12866,9 +13110,9 @@ } }, "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" }, "ee-first": { "version": "1.1.1", @@ -13379,15 +13623,6 @@ "pend": "~1.2.0" } }, - "fetch-blob": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz", - "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -13500,6 +13735,11 @@ "mime-types": "^2.1.12" } }, + "form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13731,33 +13971,36 @@ } }, "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" + "version": "12.5.3", + "resolved": "https://registry.npmjs.org/got/-/got-12.5.3.tgz", + "integrity": "sha512-8wKnb9MGU8IPGRIo+/ukTy9XLJBwDiCpIf5TVzQ9Cpol50eMTpBq2GAuDsuDIz7hTYmZgMgC1e9ydr6kSDWs3w==", + "requires": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.1", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" }, "dependencies": { "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" } } }, + "got-fetch": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/got-fetch/-/got-fetch-5.1.4.tgz", + "integrity": "sha512-Pdv+SSgtTCDZbNuGkOf0c0FZy5Syqf225JEgG/vKt13HAe8r2vNJ11DxKKODsmr7L7/6ff3lf+AxpBtD5k/dRQ==", + "requires": {} + }, "graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", @@ -13797,9 +14040,9 @@ "peer": true }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "http-errors": { "version": "1.8.1", @@ -13824,6 +14067,15 @@ "debug": "4" } }, + "http2-wrapper": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", + "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + } + }, "https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -14109,9 +14361,9 @@ } }, "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "json-parse-better-errors": { "version": "1.0.2", @@ -14144,11 +14396,11 @@ "dev": true }, "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", "requires": { - "json-buffer": "3.0.0" + "json-buffer": "3.0.1" } }, "kind-of": { @@ -14254,11 +14506,6 @@ "p-locate": "^5.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -14289,9 +14536,9 @@ } }, "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" }, "lru-cache": { "version": "6.0.0", @@ -14467,9 +14714,9 @@ "dev": true }, "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==" }, "mini-css-extract-plugin": { "version": "2.6.0", @@ -14638,19 +14885,10 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" - }, - "node-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", - "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", - "requires": { - "data-uri-to-buffer": "^3.0.1", - "fetch-blob": "^3.1.2" - } + "node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, "node-releases": { "version": "2.0.2", @@ -14671,9 +14909,9 @@ "dev": true }, "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", + "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==" }, "npm": { "version": "8.5.5", @@ -16367,41 +16605,6 @@ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "dev": true }, - "oidc-client": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", - "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", - "requires": { - "acorn": "^7.4.1", - "base64-js": "^1.5.1", - "core-js": "^3.8.3", - "crypto-js": "^4.0.0", - "serialize-javascript": "^4.0.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "requires": { - "randombytes": "^2.1.0" - } - } - } - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -16442,9 +16645,9 @@ } }, "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" }, "p-limit": { "version": "3.1.0", @@ -17156,7 +17359,7 @@ "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==" }, "prettier": { "version": "2.6.0", @@ -17348,8 +17551,7 @@ "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, "randombytes": { "version": "2.0.3", @@ -17463,6 +17665,11 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -17487,11 +17694,11 @@ "dev": true }, "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "requires": { - "lowercase-keys": "^1.0.0" + "lowercase-keys": "^3.0.0" } }, "reusify": { @@ -18176,7 +18383,7 @@ "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "requires": { "prepend-http": "^2.0.0" } @@ -18194,7 +18401,8 @@ "utility-types": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "dev": true }, "utils-merge": { "version": "1.0.1", @@ -18222,11 +18430,6 @@ "graceful-fs": "^4.1.2" } }, - "web-streams-polyfill": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", - "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==" - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index b379d007..38f9f918 100644 --- a/package.json +++ b/package.json @@ -1020,7 +1020,6 @@ "@cnblogs/prettier-config": "^2.0.3", "@types/express": "^4.17.1", "@types/glob": "^7.1.4", - "@types/lodash": "^4.14.178", "@types/lodash-es": "^4.17.6", "@types/markdown-it": "^12.2.3", "@types/mime-types": "^2.1.1", @@ -1059,6 +1058,7 @@ "ts-loader": "^9.2.5", "tsconfig-paths-webpack-plugin": "^3.5.2", "typescript": "^4.8.4", + "utility-types": "^3.10.0", "webpack": "5.70.x", "webpack-cli": "^4.8.0" }, @@ -1074,19 +1074,18 @@ "download-chromium": "^2.2.1", "express": "^4.17.1", "form-data": "^4.0.0", + "got": "^12.5.3", + "got-fetch": "^5.1.4", "is-wsl": "^2.2.0", - "lodash": "^4.17.21", "lodash-es": "^4.17.21", "markdown-it": "^12.3.2", "markdown-it-table-of-contents": "^0.6.0", "mime-types": "^2.1.34", - "node-fetch": "3.0.x", - "oidc-client": "^1.11.5", + "node-abort-controller": "^3.1.1", "puppeteer-core": "^13.5.1", "randomstring": "^1.1.4", "react": "^17.0.2", "react-dom": "^17.0.2", - "sanitize-filename": "^1.6.3", - "utility-types": "^3.10.0" + "sanitize-filename": "^1.6.3" } } diff --git a/src/authentication/access-token.ts b/src/authentication/access-token.ts new file mode 100644 index 00000000..822234ac --- /dev/null +++ b/src/authentication/access-token.ts @@ -0,0 +1,3 @@ +export interface AccessToken { + exp?: number; +} diff --git a/src/authentication/account-information.ts b/src/authentication/account-information.ts new file mode 100644 index 00000000..492b4f7b --- /dev/null +++ b/src/authentication/account-information.ts @@ -0,0 +1,61 @@ +import { UserInformationSpec } from '@/services/oauth.api'; +import { trim } from 'lodash-es'; +import { AuthenticationSessionAccountInformation } from 'vscode'; +import { CnblogsAuthenticationProvider } from './authentication-provider'; + +export class CnblogsAccountInformation implements AuthenticationSessionAccountInformation { + readonly label: string; + readonly id: string; + + private _blogApp?: string | null; + + /** + * Creates an instance of {@link CnblogsAccountInformation}. + * @param {string} [name='unknown'] + * @param {string} [avatar=''] + * @param {string} [website=''] The user blog home page url + * @param {number} [blogId=-1] + * @param {string} [sub=''] UserId(data type is Guid) + * @param {number} [accountId=-1] SpaceUserId + */ + private constructor( + public readonly name: string, + public readonly avatar: string, + public readonly website: string, + public readonly blogId: number, + public readonly sub: string, + public readonly accountId: number + ) { + this.id = `${this.accountId}-${CnblogsAuthenticationProvider.providerId}`; + this.label = name; + } + + get userId() { + return this.sub; + } + + get blogApp(): string | null { + if (this._blogApp == null) this._blogApp = this.parseBlogApp(); + + return this._blogApp; + } + + static parse(userInfo: Partial = {}) { + return new CnblogsAccountInformation( + userInfo.name || 'anonymous', + userInfo.picture || userInfo.avatar || '', + userInfo.website || '', + userInfo.blog_id ? parseInt(userInfo.blog_id, 10) : userInfo.blogId ?? -1, + userInfo.sub || '', + userInfo.account_id ? parseInt(userInfo.account_id, 10) : userInfo.accountId ?? -1 + ); + } + + private parseBlogApp() { + return ( + trim(this.website ?? '', '/') + .split('/') + .pop() ?? null + ); + } +} diff --git a/src/authentication/account-manager.ts b/src/authentication/account-manager.ts new file mode 100644 index 00000000..2a53a7e2 --- /dev/null +++ b/src/authentication/account-manager.ts @@ -0,0 +1,131 @@ +import { CnblogsAccountInformation } from './account-information'; +import { globalContext } from '../services/global-state'; +import vscode, { authentication, AuthenticationGetSessionOptions, Disposable } from 'vscode'; +import { accountViewDataProvider } from '../tree-view-providers/account-view-data-provider'; +import { postsDataProvider } from '../tree-view-providers/posts-data-provider'; +import { postCategoriesDataProvider } from '../tree-view-providers/post-categories-tree-data-provider'; +import { OauthApi } from '@/services/oauth.api'; +import { CnblogsAuthenticationProvider } from '@/authentication/authentication-provider'; +import { CnblogsAuthenticationSession } from '@/authentication/session'; + +const isAuthorizedStorageKey = 'isAuthorized'; + +class AccountManager extends vscode.Disposable { + // eslint-disable-next-line @typescript-eslint/naming-convention + static readonly ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED = 'unauthenticated'; + // eslint-disable-next-line @typescript-eslint/naming-convention + static readonly ACQUIRE_TOKEN_REJECT_EXPIRED = 'expired'; + + private readonly _authenticationProvider: CnblogsAuthenticationProvider; + private readonly _disposable: vscode.Disposable; + + private _oauthClient?: OauthApi | null; + private _session?: CnblogsAuthenticationSession | null; + + constructor() { + super(() => { + this._disposable.dispose(); + }); + + this._disposable = Disposable.from( + (this._authenticationProvider = CnblogsAuthenticationProvider.instance), + this._authenticationProvider.onDidChangeSessions(async ({ added }) => { + this._session = null; + if (added != null && added.length > 0) await this.ensureSession(); + + await this.updateAuthorizationStatus(); + + accountViewDataProvider.fireTreeDataChangedEvent(); + postsDataProvider.fireTreeDataChangedEvent(undefined); + postCategoriesDataProvider.fireTreeDataChangedEvent(); + }) + ); + } + + get isAuthorized() { + return this._session != null; + } + + get curUser(): CnblogsAccountInformation { + return this._session?.account ?? CnblogsAccountInformation.parse(); + } + + protected get oauthClient() { + return (this._oauthClient ??= new OauthApi()); + } + + /** + * Acquire the access token. + * This will reject with a human-readable reason string if not sign-in or the token has expired. + * @returns The access token of the active session + */ + async acquireToken(): Promise { + const session = await this.ensureSession({ createIfNone: false }); + return session == null + ? Promise.reject(AccountManager.ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED) + : session.hasExpired + ? Promise.reject(AccountManager.ACQUIRE_TOKEN_REJECT_EXPIRED) + : session.accessToken; + } + + async login() { + await this.ensureSession({ createIfNone: true, forceNewSession: false }); + } + + async logout() { + if (!this.isAuthorized) return; + + const session = await authentication.getSession(CnblogsAuthenticationProvider.providerId, []); + if (session) await this._authenticationProvider.removeSession(session.id); + + // For old version compatibility, **never** remove this line + await globalContext.storage.update('user', undefined); + + if (session) { + return this.oauthClient + .revoke(session.accessToken) + .catch(console.warn) + .then(ok => (!ok ? console.warn('Revocation failed') : undefined)); + } + } + + setup() { + this.updateAuthorizationStatus().catch(console.warn); + } + + private async updateAuthorizationStatus() { + await this.ensureSession({ createIfNone: false }); + await vscode.commands.executeCommand( + 'setContext', + `${globalContext.extensionName}.${isAuthorizedStorageKey}`, + this.isAuthorized + ); + if (this.isAuthorized) { + await vscode.commands.executeCommand('setContext', `${globalContext.extensionName}.user`, { + name: this.curUser.name, + avatar: this.curUser.avatar, + }); + } + } + + private async ensureSession( + opt?: AuthenticationGetSessionOptions + ): Promise { + const session = await authentication.getSession(this._authenticationProvider.providerId, [], opt).then( + s => (s ? CnblogsAuthenticationSession.parse(s) : null), + () => null + ); + + if (session != null && session.account.accountId < 0) { + this._session = null; + await this._authenticationProvider.removeSession(session.id); + } else { + this._session = session; + } + + return this._session ?? CnblogsAuthenticationSession.parse(); + } +} + +export const accountManager = new AccountManager(); +export default accountManager; diff --git a/src/authentication/authentication-provider.ts b/src/authentication/authentication-provider.ts new file mode 100644 index 00000000..252a7baf --- /dev/null +++ b/src/authentication/authentication-provider.ts @@ -0,0 +1,303 @@ +import { CnblogsAuthenticationSession } from '@/authentication/session'; +import { generateCodeChallenge } from '@/services/code-challenge.service'; +import { isArray, isUndefined } from 'lodash-es'; +import { + authentication, + AuthenticationProvider, + AuthenticationProviderAuthenticationSessionsChangeEvent, + CancellationToken, + CancellationTokenSource, + Disposable, + env, + EventEmitter, + ProgressLocation, + Uri, + window, +} from 'vscode'; +import { globalContext } from '../services/global-state'; +import RandomString from 'randomstring'; +import { OauthApi } from '@/services/oauth.api'; +import extensionUriHandler from '@/utils/uri-handler'; +import { AlertService } from '@/services/alert.service'; +import { CnblogsAccountInformation } from '@/authentication/account-information'; +import { TokenInformation } from '@/models/token-information'; +import { Optional } from 'utility-types'; + +export class CnblogsAuthenticationProvider implements AuthenticationProvider, Disposable { + static readonly providerId = 'cnblogs'; + static readonly providerName = '博客园Cnblogs'; + + private static _instance?: CnblogsAuthenticationProvider | null; + + readonly providerId = CnblogsAuthenticationProvider.providerId; + readonly providerName = CnblogsAuthenticationProvider.providerName; + + protected readonly sessionStorageKey = `${CnblogsAuthenticationProvider.providerId}.sessions`; + protected readonly allScopes = globalContext.config.oauth.scope.split(' '); + + private _allSessions?: CnblogsAuthenticationSession[] | null; + private _oauthClient?: OauthApi | null; + private readonly _sessionChangeEmitter = + new EventEmitter(); + private readonly _disposable: Disposable; + + private constructor() { + this._disposable = Disposable.from( + this._sessionChangeEmitter, + authentication.registerAuthenticationProvider( + CnblogsAuthenticationProvider.providerId, + CnblogsAuthenticationProvider.providerName, + this, + { + supportsMultipleAccounts: false, + } + ), + this.onDidChangeSessions(() => (this._allSessions = null)) + ); + } + + static get instance() { + return (this._instance ??= new CnblogsAuthenticationProvider()); + } + + get onDidChangeSessions() { + return this._sessionChangeEmitter.event; + } + + protected get context() { + return globalContext.extensionContext; + } + + protected get secretStorage() { + return globalContext.secretsStorage; + } + + protected get config() { + return globalContext.config; + } + + protected get oauthClient() { + return (this._oauthClient ??= new OauthApi()); + } + + async getSessions(scopes?: readonly string[] | undefined): Promise { + const sessions = await this.getAllSessions(); + const parsedScopes = this.ensureScopes(scopes); + return isArray(sessions) + ? sessions + .map(x => CnblogsAuthenticationSession.parse(x)) + .filter(({ scopes: sessionScopes }) => parsedScopes.every(x => sessionScopes.includes(x))) + : []; + } + + createSession(scopes: readonly string[]): Thenable { + const parsedScopes = this.ensureScopes(scopes); + return window.withProgress( + { + title: `${globalContext.displayName} - 登录`, + cancellable: true, + location: ProgressLocation.Notification, + }, + (progress, cancellationToken) => { + let disposable: Disposable | undefined | null; + + return new Promise((resolve, reject) => { + const cancellationSource = new CancellationTokenSource(); + let isTimeout = false; + const timeoutId = setTimeout(() => { + clearTimeout(timeoutId); + isTimeout = true; + cancellationSource.cancel(); + }, /* 30min */ 1800000); + const { codeVerifier } = this.signInWithBrowser({ scopes: parsedScopes }); + progress.report({ message: '等待用户在浏览器中进行授权...' }); + + disposable = Disposable.from( + cancellationSource, + extensionUriHandler.onUri(uri => { + if (cancellationSource.token.isCancellationRequested) return; + + const { authorizationCode } = this.parseOauthCallbackUri(uri); + if (!authorizationCode) return; + + progress.report({ message: '已获得授权, 正在获取令牌...' }); + + this.oauthClient + .fetchToken({ + codeVerifier, + authorizationCode, + cancellationToken: cancellationSource.token, + }) + .then(token => + this.onAccessTokenGranted(token, { + cancellationToken: cancellationSource.token, + onStateChange(state) { + progress.report({ message: state }); + }, + }) + ) + .then(resolve) + .catch(reject); + }), + cancellationToken.onCancellationRequested(() => cancellationSource.cancel()), + cancellationSource.token.onCancellationRequested(() => { + reject(`${isTimeout ? '由于超时, ' : ''}登录操作已取消`); + }) + ); + }) + .catch(reason => Promise.reject(AlertService.error(`${reason}`))) + .finally(() => { + disposable?.dispose(); + }); + } + ); + } + + async removeSession(sessionId: string): Promise { + const data = (await this.getAllSessions()).reduce<{ + removed: CnblogsAuthenticationSession[]; + keep: CnblogsAuthenticationSession[]; + }>( + (p, c) => { + c.id === sessionId ? p.removed.push(c) : p.keep.push(c); + return p; + }, + { removed: [], keep: [] } + ); + await this.context.secrets.store(this.sessionStorageKey, JSON.stringify(data.keep)); + this._sessionChangeEmitter.fire({ removed: data.removed, added: undefined, changed: undefined }); + } + + dispose() { + this._disposable.dispose(); + } + + protected async getAllSessions(): Promise { + const legacyToken = LegacyTokenStore.getAccessToken(); + if (legacyToken != null) { + await this.onAccessTokenGranted({ accessToken: legacyToken }, { shouldFireSessionAddedEvent: false }).then( + () => LegacyTokenStore.remove(), + console.warn + ); + } + + if (this._allSessions == null) { + const sessions = JSON.parse((await this.secretStorage.get(this.sessionStorageKey)) ?? '[]') as + | CnblogsAuthenticationSession[] + | null + | undefined + | unknown; + this._allSessions = isArray(sessions) ? sessions.map(x => CnblogsAuthenticationSession.parse(x)) : []; + } + + return this._allSessions; + } + + private signInWithBrowser({ scopes }: { scopes: readonly string[] }) { + const { codeVerifier, codeChallenge } = generateCodeChallenge(); + const { clientId, responseType, authorizeEndpoint, authority, clientSecret } = this.config.oauth; + + const search = new URLSearchParams([ + ['client_id', clientId], + ['response_type', responseType], + ['redirect_uri', globalContext.extensionUrl], + ['nonce', RandomString.generate(32)], + ['code_challenge', codeChallenge], + ['code_challenge_method', 'S256'], + ['scope', scopes.join(' ')], + ['client_secret', clientSecret], + ]); + env.openExternal(Uri.parse(`${authority}${authorizeEndpoint}?${search.toString()}`)).then( + undefined, + console.warn + ); + return { codeVerifier }; + } + + private ensureScopes( + scopes: readonly string[] | null | undefined, + { default: defaultScopes = this.allScopes } = {} + ): readonly string[] { + return scopes == null || scopes.length <= 0 ? defaultScopes : scopes; + } + + private parseOauthCallbackUri(uri: Uri) { + const authorizationCode = new URLSearchParams(`?${uri.query}`).get('code'); + return { authorizationCode }; + } + + private async onAccessTokenGranted( + { accessToken, refreshToken }: TokenInformation, + { + cancellationToken, + onStateChange, + shouldFireSessionAddedEvent = true, + }: { + onStateChange?: (state: string) => void; + cancellationToken?: CancellationToken; + shouldFireSessionAddedEvent?: boolean; + } = {} + ) { + const run = (func: () => TResult, predicate = () => true): TResult | undefined => + cancellationToken?.isCancellationRequested !== true && predicate() ? func() : undefined; + + let session: CnblogsAuthenticationSession | undefined; + try { + onStateChange?.('正在获取账户信息...'); + const userInfo = await run(() => + this.oauthClient.fetchUserInformation(accessToken, { + cancellationToken: cancellationToken, + }) + ); + + onStateChange?.('即将完成...'); + session = run(() => + isUndefined(userInfo) + ? undefined + : CnblogsAuthenticationSession.parse({ + accessToken, + refreshToken, + account: CnblogsAccountInformation.parse(userInfo), + scopes: this.ensureScopes(null), + id: `${this.providerId}-${userInfo.account_id}`, + }) + ); + const hasStored = await run(() => + isUndefined(session) + ? Promise.resolve(false) + : this.secretStorage.store(this.sessionStorageKey, JSON.stringify([session])).then( + () => true, + () => false + ) + ); + run( + () => + isUndefined(session) || !shouldFireSessionAddedEvent + ? undefined + : this._sessionChangeEmitter.fire({ + added: [session], + removed: undefined, + changed: undefined, + }), + () => hasStored === true + ); + } finally { + if (session != null && cancellationToken?.isCancellationRequested) await this.removeSession(session.id); + } + if (session == null) throw new Error('Failed to create session'); + return session; + } +} + +class LegacyTokenStore { + private static readonly _key = 'user'; + + static getAccessToken() { + return globalContext.storage.get>(this._key) + ?.authorizationInfo?.accessToken; + } + + static remove() { + globalContext.storage.update(this._key, undefined).then(undefined, console.error); + } +} diff --git a/src/authentication/index.ts b/src/authentication/index.ts new file mode 100644 index 00000000..32509e6e --- /dev/null +++ b/src/authentication/index.ts @@ -0,0 +1,4 @@ +export * from './authentication-provider'; +export * from './session'; +export * from './access-token'; +export * from './account-information'; diff --git a/src/authentication/session.ts b/src/authentication/session.ts new file mode 100644 index 00000000..af90c75c --- /dev/null +++ b/src/authentication/session.ts @@ -0,0 +1,36 @@ +import { AccessToken } from '@/authentication/access-token'; +import { CnblogsAccountInformation } from '@/authentication/account-information'; +import { keys, merge, pick } from 'lodash-es'; +import { AuthenticationSession } from 'vscode'; + +export class CnblogsAuthenticationSession implements AuthenticationSession { + private _parsedAccessToken?: AccessToken | null; + + private constructor( + public readonly account: CnblogsAccountInformation, + public readonly id = '', + public readonly accessToken = '', + public readonly refreshToken = '', + public readonly scopes: readonly string[] = [] + ) {} + + get hasExpired() { + const { exp } = this.parsedAccessToken; + return typeof exp === 'number' ? exp * 1000 <= Date.now() : true; + } + + private get parsedAccessToken() { + return (this._parsedAccessToken ??= JSON.parse( + Buffer.from(this.accessToken.split('.')[1], 'base64').toString() + )) as AccessToken; + } + + static parse>(data?: T) { + const obj = new CnblogsAuthenticationSession(CnblogsAccountInformation.parse({})); + merge(obj, pick(data, keys(obj))); + + return obj.account instanceof CnblogsAccountInformation + ? obj + : merge(obj, { account: CnblogsAccountInformation.parse(obj.account) }); + } +} diff --git a/src/commands/commands-registration.ts b/src/commands/commands-registration.ts index 65fb9405..1219efe8 100644 --- a/src/commands/commands-registration.ts +++ b/src/commands/commands-registration.ts @@ -4,7 +4,7 @@ import { openMyWebBlogConsole } from './open-my-blog-management-background'; import { openMyHomePage } from './open-my-home-page'; import { login, logout } from './login'; import { openMyBlog } from './open-my-blog'; -import { globalState } from '../services/global-state'; +import { globalContext } from '../services/global-state'; import { gotoNextPostsList, gotoPreviousPostsList, @@ -39,8 +39,8 @@ import { registerCommandsForIngsList } from 'src/commands/ing/ings-list-commands import { CopyPostLinkCommandHandler } from '@/commands/posts-list/copy-link'; export const registerCommands = () => { - const context = globalState.extensionContext; - const appName = globalState.extensionName; + const context = globalContext.extensionContext; + const appName = globalContext.extensionName; const disposables = [ commands.registerCommand(`${appName}.login`, login), commands.registerCommand(`${appName}.open-my-blog`, openMyBlog), diff --git a/src/commands/extract-images.ts b/src/commands/extract-images.ts index c93e7e8b..30df667c 100644 --- a/src/commands/extract-images.ts +++ b/src/commands/extract-images.ts @@ -1,5 +1,5 @@ import { Uri, workspace, window, MessageOptions, MessageItem, ProgressLocation, Range } from 'vscode'; -import { MarkdownImage, MarkdownImagesExtractor } from '../services/images-extractor.service'; +import { ImageInformation, MarkdownImagesExtractor } from '../services/images-extractor.service'; type ExtractOption = MessageItem & Partial>; const extractOptions: readonly ExtractOption[] = [ @@ -15,7 +15,7 @@ export const extractImages = async ( ): Promise => { if (arg instanceof Uri && arg.scheme === 'file') { const shouldIgnoreWarnings = inputImageType != null; - const markdown = new TextDecoder().decode(await workspace.fs.readFile(arg)); + const markdown = (await workspace.fs.readFile(arg)).toString(); const extractor = new MarkdownImagesExtractor(markdown, arg); const images = extractor.findImages(); const availableWebImagesCount = images.filter(extractor.createImageTypeFilter('web')).length; @@ -61,12 +61,12 @@ export const extractImages = async ( const total = extractResults.length; await editor.edit(editBuilder => { for (const [range, , extractedImage] of extractResults - .filter((x): x is [source: MarkdownImage, result: MarkdownImage] => x[1] != null) + .filter((x): x is [source: ImageInformation, result: ImageInformation] => x[1] != null) .map( ([sourceImage, result]): [ range: Range | null, - sourceImage: MarkdownImage, - extractedImage: MarkdownImage + sourceImage: ImageInformation, + extractedImage: ImageInformation ] => { if (sourceImage.index == null) return [null, sourceImage, result]; diff --git a/src/commands/ing/ings-list-commands-registration.ts b/src/commands/ing/ings-list-commands-registration.ts index 23cfe242..493e915c 100644 --- a/src/commands/ing/ings-list-commands-registration.ts +++ b/src/commands/ing/ings-list-commands-registration.ts @@ -1,6 +1,6 @@ import { commands } from 'vscode'; import { RefreshIngsList } from 'src/commands/ing/refresh-ings-list'; -import { globalState } from 'src/services/global-state'; +import { globalContext } from 'src/services/global-state'; import { IDisposable } from '@fluentui/react'; import { GotoIngsListFirstPage, @@ -11,7 +11,7 @@ import { SelectIngType } from '@/commands/ing/select-ing-type'; import { OpenIngInBrowser } from '@/commands/ing/open-ing-in-browser'; export const registerCommandsForIngsList = (disposables: IDisposable[]) => { - const appName = globalState.extensionName; + const appName = globalContext.extensionName; disposables.push( ...[ diff --git a/src/commands/ing/open-ing-in-browser.ts b/src/commands/ing/open-ing-in-browser.ts index c158012c..884ed996 100644 --- a/src/commands/ing/open-ing-in-browser.ts +++ b/src/commands/ing/open-ing-in-browser.ts @@ -1,9 +1,9 @@ import { CommandHandler } from '@/commands/command-handler'; -import { globalState } from '@/services/global-state'; +import { globalContext } from '@/services/global-state'; import { commands, Uri } from 'vscode'; export class OpenIngInBrowser extends CommandHandler { async handle(): Promise { - await commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite)); + await commands.executeCommand('vscode.open', Uri.parse(globalContext.config.ingSite)); } } diff --git a/src/commands/ing/publish-ing.ts b/src/commands/ing/publish-ing.ts index 6cd20602..9c803b5f 100644 --- a/src/commands/ing/publish-ing.ts +++ b/src/commands/ing/publish-ing.ts @@ -1,7 +1,7 @@ import { CommandHandler } from '@/commands/command-handler'; import { IngPublishModel, IngType } from '@/models/ing'; import { AlertService } from '@/services/alert.service'; -import { globalState } from '@/services/global-state'; +import { globalContext } from '@/services/global-state'; import { IngApi } from '@/services/ing.api'; import { IngsListWebviewProvider } from '@/services/ings-list-webview-provider'; import { InputStep, MultiStepInput, QuickPickParameters } from '@/services/multi-step-input'; @@ -163,25 +163,26 @@ export class PublishIngCommandHandler extends CommandHandler { const options = [ [ '打开闪存', - (): Thenable => commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite)), + (): Thenable => + commands.executeCommand('vscode.open', Uri.parse(globalContext.config.ingSite)), ], [ '我的闪存', (): Thenable => - commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite + '/#my')), + commands.executeCommand('vscode.open', Uri.parse(globalContext.config.ingSite + '/#my')), ], [ '新回应', (): Thenable => commands.executeCommand( 'vscode.open', - Uri.parse(globalState.config.ingSite + '/#recentcomment') + Uri.parse(globalContext.config.ingSite + '/#recentcomment') ), ], [ '提到我', (): Thenable => - commands.executeCommand('vscode.open', Uri.parse(globalState.config.ingSite + '/#mention')), + commands.executeCommand('vscode.open', Uri.parse(globalContext.config.ingSite + '/#mention')), ], ] as const; const option = await window.showInformationMessage( diff --git a/src/commands/login.ts b/src/commands/login.ts index 60a5e058..b2c089ce 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,5 +1,5 @@ -import { accountService } from '../services/account.service'; +import { accountManager } from '../authentication/account-manager'; -export const login = () => accountService.login(); +export const login = () => accountManager.login(); -export const logout = () => accountService.logout(); +export const logout = () => accountManager.logout(); diff --git a/src/commands/open-my-blog.ts b/src/commands/open-my-blog.ts index faf582b0..b5d4f9b9 100644 --- a/src/commands/open-my-blog.ts +++ b/src/commands/open-my-blog.ts @@ -1,7 +1,7 @@ -import { accountService } from '../services/account.service'; +import { accountManager } from '../authentication/account-manager'; import vscode from 'vscode'; export const openMyBlog = () => { - const userBlogUrl = accountService.curUser?.website; + const userBlogUrl = accountManager.curUser?.website; if (userBlogUrl) return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(userBlogUrl)); }; diff --git a/src/commands/open-my-home-page.ts b/src/commands/open-my-home-page.ts index 7f36765d..76062be0 100644 --- a/src/commands/open-my-home-page.ts +++ b/src/commands/open-my-home-page.ts @@ -1,8 +1,8 @@ -import { accountService } from '../services/account.service'; +import { accountManager } from '../authentication/account-manager'; import vscode from 'vscode'; export const openMyHomePage = () => { - const { accountId } = accountService.curUser; + const { accountId } = accountManager.curUser; if (!accountId || accountId <= 0) return; const userHomePageUrl = `https://home.cnblogs.com/u/${accountId}`; diff --git a/src/commands/pdf/export-pdf.command.ts b/src/commands/pdf/export-pdf.command.ts index b947c1b1..063321e4 100644 --- a/src/commands/pdf/export-pdf.command.ts +++ b/src/commands/pdf/export-pdf.command.ts @@ -10,7 +10,7 @@ import { extensionViews } from '../../tree-view-providers/tree-view-registration import { postPdfTemplateBuilder } from './post-pdf-template-builder'; import { chromiumPathProvider } from '../../utils/chromium-path-provider'; import { Settings } from '../../services/settings.service'; -import { accountService } from '../../services/account.service'; +import { accountManager } from '../../authentication/account-manager'; import { AlertService } from '../../services/alert.service'; import { PostTreeItem } from '../../tree-view-providers/models/post-tree-item'; import { PostEditDto } from '@/models/post-edit-dto'; @@ -158,7 +158,7 @@ const handleUriInput = async (uri: Uri): Promise => { Object.assign(inputPost, { id: -1, title: path.basename(fsPath, path.extname(fsPath)), - postBody: new TextDecoder().decode(await workspace.fs.readFile(uri)), + postBody: Buffer.from(await workspace.fs.readFile(uri)).toString(), } as Post); } @@ -183,7 +183,7 @@ const exportPostToPdf = async (input: Post | PostTreeItem | Uri): Promise const { curUser: { blogApp }, - } = accountService; + } = accountManager; if (!blogApp) return AlertService.warning('无法获取到博客地址, 请检查登录状态'); diff --git a/src/commands/pdf/post-pdf-template-builder.ts b/src/commands/pdf/post-pdf-template-builder.ts index 76a1d113..9169ffd5 100644 --- a/src/commands/pdf/post-pdf-template-builder.ts +++ b/src/commands/pdf/post-pdf-template-builder.ts @@ -3,7 +3,7 @@ import { PostFileMapManager } from '../../services/post-file-map'; import fs from 'fs'; import { markdownItFactory } from '@cnblogs/markdown-it-presets'; import { blogSettingsService } from '../../services/blog-settings.service'; -import { accountService } from '../../services/account.service'; +import { accountManager } from '../../authentication/account-manager'; import { postCategoryService } from '../../services/post-category.service'; import { PostCategory } from '../../models/post-category'; @@ -62,7 +62,7 @@ export namespace postPdfTemplateBuilder { enableCodeLineNumber: isCodeLineNumberEnabled, blogId, } = await blogSettingsService.getBlogSettings(); - const { userId } = accountService.curUser; + const { userId } = accountManager.curUser; return ` ${post.title} diff --git a/src/commands/posts-list/create-local-draft.ts b/src/commands/posts-list/create-local-draft.ts index 85700585..fa624a8d 100644 --- a/src/commands/posts-list/create-local-draft.ts +++ b/src/commands/posts-list/create-local-draft.ts @@ -1,5 +1,5 @@ import { homedir } from 'os'; -import path = require('path'); +import path from 'path'; import { Uri, window, workspace } from 'vscode'; import { Settings } from '../../services/settings.service'; import { revealActiveFileInExplorer } from '../../utils/reveal-active-file'; @@ -28,7 +28,7 @@ export const createLocalDraft = async () => { await workspace.fs.stat(Uri.file(filePath)); } catch (e) { // 文件不存在 - await workspace.fs.writeFile(Uri.file(filePath), new TextEncoder().encode('')); + await workspace.fs.writeFile(Uri.file(filePath), Buffer.from('')); } await openPostFile(filePath); diff --git a/src/commands/posts-list/open-post-in-vscode.ts b/src/commands/posts-list/open-post-in-vscode.ts index 61d7ba38..ce8e8ad8 100644 --- a/src/commands/posts-list/open-post-in-vscode.ts +++ b/src/commands/posts-list/open-post-in-vscode.ts @@ -76,7 +76,7 @@ export const openPostInVscode = async (postId: number, forceUpdateLocalPostFile } // 博文内容写入本地文件, 若文件不存在, 会自动创建对应的文件 - await workspace.fs.writeFile(fileUri, new TextEncoder().encode(postEditDto.post.postBody)); + await workspace.fs.writeFile(fileUri, Buffer.from(postEditDto.post.postBody)); await PostFileMapManager.updateOrCreate(postId, fileUri.fsPath); await openPostFile(post); return fileUri; diff --git a/src/commands/posts-list/refresh-posts-list.ts b/src/commands/posts-list/refresh-posts-list.ts index e4a3be78..df4c7e0f 100644 --- a/src/commands/posts-list/refresh-posts-list.ts +++ b/src/commands/posts-list/refresh-posts-list.ts @@ -1,4 +1,4 @@ -import { globalState } from '../../services/global-state'; +import { globalContext } from '../../services/global-state'; import { postService } from '../../services/post.service'; import vscode from 'vscode'; import { postsDataProvider } from '../../tree-view-providers/posts-data-provider'; @@ -86,7 +86,7 @@ export const seekPostsList = async () => { let isRefreshing = false; const setRefreshing = async (value = false) => { - const extName = globalState.extensionName; + const extName = globalContext.extensionName; await vscode.commands .executeCommand('setContext', `${extName}.posts-list.refreshing`, value) .then(undefined, () => false); @@ -94,7 +94,7 @@ const setRefreshing = async (value = false) => { }; const setPostListContext = async (pageCount: number, hasPrevious: boolean, hasNext: boolean) => { - const extName = globalState.extensionName; + const extName = globalContext.extensionName; await vscode.commands.executeCommand('setContext', `${extName}.posts-list.hasPrevious`, hasPrevious); await vscode.commands.executeCommand('setContext', `${extName}.posts-list.hasNext`, hasNext); await vscode.commands.executeCommand('setContext', `${extName}.posts-list.pageCount`, pageCount); diff --git a/src/commands/posts-list/rename-post.ts b/src/commands/posts-list/rename-post.ts index cd76adae..92a3e8a5 100644 --- a/src/commands/posts-list/rename-post.ts +++ b/src/commands/posts-list/rename-post.ts @@ -1,5 +1,5 @@ import { escapeRegExp } from 'lodash-es'; -import path = require('path'); +import path from 'path'; import { MessageOptions, ProgressLocation, Uri, window, workspace } from 'vscode'; import { Post } from '../../models/post'; import { postService } from '../../services/post.service'; diff --git a/src/commands/posts-list/save-post.ts b/src/commands/posts-list/save-post.ts index c42c4f2e..984989b9 100644 --- a/src/commands/posts-list/save-post.ts +++ b/src/commands/posts-list/save-post.ts @@ -64,13 +64,10 @@ export const savePostFileToCnblogs = async (fileUri: Uri | undefined) => { await PostFileMapManager.updateOrCreate(selectedPost.id, filePath); const postEditDto = await postService.fetchPostEditDto(selectedPost.id); if (postEditDto) { - const fileContent = new TextDecoder().decode(await workspace.fs.readFile(fileUri)); - if (!fileContent) { - await workspace.fs.writeFile( - fileUri, - new TextEncoder().encode(postEditDto.post.postBody) - ); - } + const fileContent = Buffer.from(await workspace.fs.readFile(fileUri)).toString(); + if (!fileContent) + await workspace.fs.writeFile(fileUri, Buffer.from(postEditDto.post.postBody)); + await savePostToCnblogs(postEditDto.post); } } @@ -91,7 +88,7 @@ export const saveLocalDraftToCnblogs = async (localDraft: LocalDraft) => { AlertService.warning('不受支持的文件格式! 只支持markdown格式'); return; } - const editDto = await postService.fetchPostEditDtoTemplate(); + const editDto = await postService.fetchPostEditTemplate(); if (!editDto) return; const { post } = editDto; @@ -145,7 +142,7 @@ export const savePostToCnblogs = async (input: Post | PostTreeItem | PostEditDto AlertService.warning('本地无该博文的编辑记录'); return; } - const updatedPostBody = new TextDecoder().decode(await workspace.fs.readFile(Uri.file(localFilePath))); + const updatedPostBody = Buffer.from(await workspace.fs.readFile(Uri.file(localFilePath))).toString(); post.postBody = updatedPostBody; post.title = await PostTitleSanitizer.unSanitize(post); } diff --git a/src/commands/pull-post-remote-updates.ts b/src/commands/pull-post-remote-updates.ts index 8f3d25b7..025bd353 100644 --- a/src/commands/pull-post-remote-updates.ts +++ b/src/commands/pull-post-remote-updates.ts @@ -84,7 +84,7 @@ const update = async (contexts: CommandContext[]) => { if (post) { const textEditors = window.visibleTextEditors.filter(x => x.document.uri.fsPath === fileUri.fsPath); await Promise.all(textEditors.map(editor => editor.document.save())); - await workspace.fs.writeFile(fileUri, new TextEncoder().encode(post.postBody)); + await workspace.fs.writeFile(fileUri, Buffer.from(post.postBody)); } } }; diff --git a/src/commands/show-local-file-to-post-info.ts b/src/commands/show-local-file-to-post-info.ts index 12c354ad..f051ee6a 100644 --- a/src/commands/show-local-file-to-post-info.ts +++ b/src/commands/show-local-file-to-post-info.ts @@ -1,4 +1,4 @@ -import path = require('path'); +import path from 'path'; import { MessageOptions, Uri, window } from 'vscode'; import { AlertService } from '../services/alert.service'; import { postService } from '../services/post.service'; diff --git a/src/commands/upload-image/upload-local-disk-image.ts b/src/commands/upload-image/upload-local-disk-image.ts index c35456e4..4cf03363 100644 --- a/src/commands/upload-image/upload-local-disk-image.ts +++ b/src/commands/upload-image/upload-local-disk-image.ts @@ -1,5 +1,5 @@ import { ProgressLocation, window } from 'vscode'; -import { imageService } from '../../services/image.service'; +import { imageService } from '@/services/image.service'; import fs from 'fs'; export const uploadLocalDiskImage = async () => { diff --git a/src/extension.ts b/src/extension.ts index 76e2f451..3ea26d1f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,24 +1,24 @@ import { registerTreeViews } from '@/tree-view-providers/tree-view-registration'; import { registerCommands } from '@/commands/commands-registration'; -import { globalState } from '@/services/global-state'; +import { globalContext } from '@/services/global-state'; // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import vscode from 'vscode'; -import { accountService } from '@/services/account.service'; +import { accountManager } from '@/authentication/account-manager'; import { observeConfigurationChange, observeWorkspaceFolderAndFileChange as observeWorkspaceFolderChange, } from '@/services/check-workspace'; -import { EditPostUriHandler } from '@/services/edit-post-uri-handler'; +import extensionUriHandler from '@/utils/uri-handler'; import { IngsListWebviewProvider } from 'src/services/ings-list-webview-provider'; import { extendMarkdownIt } from '@/markdown/extend-markdownIt'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { - globalState.extensionContext = context; - void accountService.setIsAuthorizedToContext(); - context.subscriptions.push(accountService); + globalContext.extensionContext = context; + accountManager.setup(); + context.subscriptions.push(accountManager); registerCommands(); registerTreeViews(); @@ -27,7 +27,7 @@ export function activate(context: vscode.ExtensionContext) { }, 1000); observeConfigurationChange(); observeWorkspaceFolderChange(); - vscode.window.registerUriHandler(new EditPostUriHandler()); + vscode.window.registerUriHandler(extensionUriHandler); return { extendMarkdownIt, }; diff --git a/src/markdown/markdown.entry.ts b/src/markdown/markdown.entry.ts index 6b702280..dca39ff0 100644 --- a/src/markdown/markdown.entry.ts +++ b/src/markdown/markdown.entry.ts @@ -1,3 +1,5 @@ +/// + import { HighlightersFactory, HljsHighlighter } from '@cnblogs/code-highlight-adapter'; HighlightersFactory.configCodeHighlightOptions({ enableCodeLineNumber: false }); diff --git a/src/models/config.ts b/src/models/config.ts index 27a37f0d..a0c37de7 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -1,7 +1,7 @@ import { env } from 'process'; -export interface IConfig { - oauth: { +export interface IExtensionConfig { + readonly oauth: Readonly<{ authority: string; tokenEndpoint: string; authorizeEndpoint: string; @@ -11,15 +11,15 @@ export interface IConfig { responseType: string; scope: string; revocationEndpoint: string; - }; - apiBaseUrl: string; - ingSite: string; - cnblogsOpenApiUrl: string; + }>; + readonly apiBaseUrl: string; + readonly ingSite: string; + readonly cnblogsOpenApiUrl: string; } export const isDev = () => process.env.NODE_ENV === 'Development'; -export const defaultConfig: IConfig = { +export const defaultConfig: IExtensionConfig = { oauth: { authority: 'https://oauth.cnblogs.com', tokenEndpoint: '/connect/token', @@ -36,13 +36,15 @@ export const defaultConfig: IConfig = { cnblogsOpenApiUrl: 'https://api.cnblogs.com', }; -export const devConfig = Object.assign({}, defaultConfig, { - oauth: Object.assign({}, defaultConfig.oauth, { +export const devConfig: IExtensionConfig = { + ...defaultConfig, + oauth: { + ...defaultConfig.oauth, authority: env.Authority ? env.Authority : 'https://my-oauth.cnblogs.com', clientId: env.ClientId ? env.ClientId : 'vscode-cnb', clientSecret: env.ClientSecret ? env.ClientSecret : '', - }), + }, apiBaseUrl: 'https://admin.cnblogs.com', ingSite: 'https://my-ing.cnblogs.com', cnblogsOpenApiUrl: 'https://my-api.cnblogs.com', -}); +}; diff --git a/src/models/post-updated-response.ts b/src/models/post-updated-response.ts index b776d222..3e4eb8fb 100644 --- a/src/models/post-updated-response.ts +++ b/src/models/post-updated-response.ts @@ -1,3 +1,4 @@ +import { merge } from 'lodash-es'; import { PostType } from './post'; export class PostUpdatedResponse { @@ -8,4 +9,8 @@ export class PostUpdatedResponse { postType: PostType = PostType.blogPost; dateAdded: Date = new Date(); entryName = ''; + + static parse(data: T): PostUpdatedResponse { + return merge(new PostUpdatedResponse(), data); + } } diff --git a/src/models/token-information.ts b/src/models/token-information.ts new file mode 100644 index 00000000..6d900861 --- /dev/null +++ b/src/models/token-information.ts @@ -0,0 +1,7 @@ +export interface TokenInformation { + idToken?: string; + accessToken: string; + refreshToken?: string; + expiresIn?: number; + tokenType?: string; +} diff --git a/src/models/user-settings.ts b/src/models/user-settings.ts deleted file mode 100644 index 639a0b96..00000000 --- a/src/models/user-settings.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { trim } from 'lodash-es'; - -export class UserAuthorizationInfo { - constructor(public idToken: string, public accessToken: string, expiresIn: number, public tokenType: string) {} -} - -export class UserInfo { - private _blogApp?: string | null; - - /** - * Creates an instance of UserInfo. - * @param {UserAuthorizationInfo} [authorizationInfo] - * @param {string} [name='unknown'] - * @param {string} [avatar=''] - * @param {string} [website=''] The user blog home page url - * @param {number} [blogId=-1] - * @param {string} [sub=''] UserId(data type is Guid) - * @param {number} [accountId=-1] SpaceUserId - */ - constructor( - public authorizationInfo?: UserAuthorizationInfo, - public name: string = 'unknown', - public avatar: string = '', - public website: string = '', - public blogId: number = -1, - public sub: string = '', - public accountId: number = -1 - ) {} - - get userId() { - return this.sub; - } - - get blogApp(): string | null { - if (this._blogApp == null) this._blogApp = this.parseBlogApp(); - - return this._blogApp; - } - - private parseBlogApp(): string | null { - return ( - trim(this.website ?? '', '/') - .split('/') - .pop() ?? null - ); - } -} diff --git a/src/services/account.service.ts b/src/services/account.service.ts deleted file mode 100644 index 6af2d215..00000000 --- a/src/services/account.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { convertObjectKeysToCamelCase } from './fetch-json-response-to-camel-case'; -import { UserAuthorizationInfo, UserInfo } from './../models/user-settings'; -import { globalState } from './global-state'; -import { CnblogsOAuthService } from './cnblogs-oauth.service'; -import vscode from 'vscode'; -import { URLSearchParams } from 'url'; -import { generateCodeChallenge } from './code-challenge.service'; -import RandomString from 'randomstring'; -import fetch from 'node-fetch'; -import { accountViewDataProvider } from '../tree-view-providers/account-view-data-provider'; -import { postsDataProvider } from '../tree-view-providers/posts-data-provider'; -import { postCategoriesDataProvider } from '../tree-view-providers/post-categories-tree-data-provider'; -import { checkIsAccessTokenExpired } from '../utils/check-access-token-expired'; - -const isAuthorizedStorageKey = 'isAuthorized'; - -export class AccountService extends vscode.Disposable { - private static _instance: AccountService = new AccountService(); - - private _curUser?: UserInfo; - private _oauthServ = new CnblogsOAuthService(); - - protected constructor() { - super((): void => this._oauthServ.dispose()); - } - - static get instance() { - return this._instance; - } - - get isAuthorized() { - return globalState.storage.get(isAuthorizedStorageKey); - } - - get curUser(): UserInfo { - return ( - this._curUser || - (this._curUser = Object.assign(new UserInfo(), globalState.storage.get('user') ?? new UserInfo())) - ); - } - - buildBearerAuthorizationHeader(accessToken?: string): [string, string] { - accessToken ??= this.curUser.authorizationInfo?.accessToken ?? ''; - const hasExpired = checkIsAccessTokenExpired(accessToken); - if (hasExpired) void Promise.all([this.logout(), this.alertLoginStatusExpired()]); - - return ['Authorization', `Bearer ${hasExpired ? '' : accessToken}`]; - } - - async login() { - const { codeVerifier, codeChallenge } = generateCodeChallenge(); - this._oauthServ.startListenAuthorizationCodeCallback(codeVerifier, (authorizationInfo, err) => { - if (authorizationInfo && !err) return this.handleAuthorized(authorizationInfo); - }); - const { clientId, responseType, scope, authorizeEndpoint, authority, clientSecret } = globalState.config.oauth; - const search = new URLSearchParams([ - ['client_id', clientId], - ['response_type', responseType], - ['redirect_uri', this._oauthServ.listenUrl], - ['nonce', RandomString.generate(32)], - ['code_challenge', codeChallenge], - ['code_challenge_method', 'S256'], - ['scope', scope], - ['client_secret', clientSecret], - ]); - await vscode.commands.executeCommand( - 'vscode.open', - vscode.Uri.parse(`${authority}${authorizeEndpoint}?${search.toString()}`) - ); - accountViewDataProvider.fireTreeDataChangedEvent(); - postsDataProvider.fireTreeDataChangedEvent(undefined); - postCategoriesDataProvider.fireTreeDataChangedEvent(); - } - - async logout() { - if (!this.isAuthorized) return; - - const { clientId, revocationEndpoint, authority } = globalState.config.oauth; - const token = this.curUser?.authorizationInfo?.accessToken; - - await globalState.storage.update('user', {}); - this._curUser = undefined; - await this.setIsAuthorized(false); - - if (token) { - const body = new URLSearchParams([ - ['client_id', clientId], - ['token', token], - ['token_type_hint', 'access_token'], - ]); - const url = `${authority}${revocationEndpoint}`; - const res = await fetch(url, { - method: 'POST', - body: body, - headers: [ - this.buildBearerAuthorizationHeader(token), - ['Content-Type', 'application/x-www-form-urlencoded'], - ], - }); - if (!res.ok) console.warn('Revocation failed', res); - } - } - - async refreshUserInfo() { - if (this.curUser && this.curUser.authorizationInfo) - await this.fetchAndStoreUserInfo(this.curUser.authorizationInfo); - } - - async setIsAuthorizedToContext() { - await vscode.commands.executeCommand( - 'setContext', - `${globalState.extensionName}.${isAuthorizedStorageKey}`, - this.isAuthorized - ); - if (this.isAuthorized) { - await vscode.commands.executeCommand('setContext', `${globalState.extensionName}.user`, { - name: this.curUser.name, - avatar: this.curUser.avatar, - }); - } - } - - private async setIsAuthorized(authorized = false): Promise { - await globalState.storage.update(isAuthorizedStorageKey, authorized); - await this.setIsAuthorizedToContext(); - } - - private async handleAuthorized(authorizationInfo: UserAuthorizationInfo) { - await this.fetchAndStoreUserInfo(authorizationInfo); - await this.setIsAuthorized(true); - } - - private async fetchAndStoreUserInfo(authorizationInfo: UserAuthorizationInfo) { - const userInfo = await this.fetchUserInfo(authorizationInfo); - await globalState.storage.update('user', userInfo); - return userInfo; - } - - private async fetchUserInfo(authorizationInfo: UserAuthorizationInfo): Promise { - const { authority, userInfoEndpoint } = globalState.config.oauth; - const res = await fetch(`${authority}${userInfoEndpoint}`, { - method: 'GET', - headers: [this.buildBearerAuthorizationHeader(authorizationInfo.accessToken)], - }); - const obj = convertObjectKeysToCamelCase((await res.json()) as Record); - return Object.assign(new UserInfo(authorizationInfo), { ...obj, avatar: obj.picture }); - } - - private async alertLoginStatusExpired() { - const options = ['登录']; - const input = await vscode.window.showInformationMessage( - '登录状态已过期, 请重新登录', - { modal: true } as vscode.MessageOptions, - ...options - ); - if (input === options[0]) await this.login(); - } -} - -export const accountService = AccountService.instance; diff --git a/src/services/alert.service.ts b/src/services/alert.service.ts index 82652010..b98bdcf5 100644 --- a/src/services/alert.service.ts +++ b/src/services/alert.service.ts @@ -23,4 +23,14 @@ export class AlertService { file = trimExt ? path.basename(file, path.extname(file)) : file; this.warning(`本地文件"${file}"未关联博客园博文`); } + + static async alertUnauthenticated({ onLoginActionHook }: { onLoginActionHook?: () => unknown } = {}) { + const options = ['立即登录']; + const input = await vscode.window.showWarningMessage( + '登录状态已过期, 请重新登录', + { modal: true } as vscode.MessageOptions, + ...options + ); + if (input === options[0]) onLoginActionHook?.(); + } } diff --git a/src/services/blog-settings.service.ts b/src/services/blog-settings.service.ts index 6b76081a..360d8448 100644 --- a/src/services/blog-settings.service.ts +++ b/src/services/blog-settings.service.ts @@ -1,7 +1,6 @@ -import fetch from 'node-fetch'; +import fetch from '@/utils/fetch-client'; import { BlogSettings, BlogSiteDto, BlogSiteExtendDto } from '../models/blog-settings'; -import { accountService } from './account.service'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; export class BlogSettingsService { private static _instance?: BlogSettingsService; @@ -18,10 +17,8 @@ export class BlogSettingsService { async getBlogSettings(forceRefresh = false): Promise { if (this._settings && !forceRefresh) return this._settings; - const url = `${globalState.config.apiBaseUrl}/api/settings`; - const res = await fetch(url, { - headers: [accountService.buildBearerAuthorizationHeader()], - }); + const url = `${globalContext.config.apiBaseUrl}/api/settings`; + const res = await fetch(url); if (!res.ok) throw Error(`Failed to request ${url}, statusCode: ${res.status}, detail: ${await res.text()}`); const data = (await res.json()) as { blogSite: BlogSiteDto; extend: BlogSiteExtendDto }; diff --git a/src/services/check-workspace.ts b/src/services/check-workspace.ts index fca1d640..03dc15fa 100644 --- a/src/services/check-workspace.ts +++ b/src/services/check-workspace.ts @@ -1,19 +1,19 @@ import { commands, workspace } from 'vscode'; import { refreshPostCategoriesList } from '../commands/post-category/refresh-post-categories-list'; import { refreshPostsList } from '../commands/posts-list/refresh-posts-list'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; import { PostFileMapManager } from './post-file-map'; import { Settings } from './settings.service'; export const isTargetWorkspace = (): boolean => { const folders = workspace.workspaceFolders; const isTarget = !!folders && folders.length === 1 && folders[0].uri.path === Settings.workspaceUri.path; - void commands.executeCommand('setContext', `${globalState.extensionName}.isTargetWorkspace`, isTarget); + void commands.executeCommand('setContext', `${globalContext.extensionName}.isTargetWorkspace`, isTarget); return isTarget; }; export const observeConfigurationChange = () => { - globalState.extensionContext?.subscriptions.push( + globalContext.extensionContext?.subscriptions.push( workspace.onDidChangeConfiguration(ev => { if (ev.affectsConfiguration(Settings.prefix)) isTargetWorkspace(); @@ -31,7 +31,7 @@ export const observeConfigurationChange = () => { }; export const observeWorkspaceFolderAndFileChange = () => { - globalState.extensionContext?.subscriptions.push( + globalContext.extensionContext?.subscriptions.push( workspace.onDidRenameFiles(e => { for (const item of e.files) { const { oldUri, newUri } = item; diff --git a/src/services/cnblogs-oauth.service.ts b/src/services/cnblogs-oauth.service.ts deleted file mode 100644 index 4ad47c39..00000000 --- a/src/services/cnblogs-oauth.service.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { UserAuthorizationInfo } from './../models/user-settings'; -import { convertObjectKeysToCamelCase } from './fetch-json-response-to-camel-case'; -import express from 'express'; -import { Server } from 'http'; -import fetch from 'node-fetch'; -import { URLSearchParams } from 'url'; -import { globalState } from './global-state'; -import { Disposable } from 'vscode'; - -export class CnblogsOAuthService extends Disposable { - listenPort = 41385; - private _app: express.Express; - private _server?: Server; - private _codeVerifier = ''; - - constructor() { - super(() => { - if (this._server && this._server.listening) this._server?.close(); - }); - this._app = express(); - } - - get listenUrl() { - return `http://localhost:${this.listenPort}`; - } - - startListenAuthorizationCodeCallback( - codeVerifier: string, - callback: (authorizationInfo?: UserAuthorizationInfo, error?: any) => void | Promise - ) { - if (this._server) this._server.close(); - - this._server = this._app.listen(this.listenPort); - this._codeVerifier = codeVerifier; - this._app.get( - ['/', '/callback'], - (req, res) => - void this.handleOAuthRedirect(req, res).then(authorizationInfo => - authorizationInfo instanceof UserAuthorizationInfo - ? callback(authorizationInfo) - : callback(undefined, authorizationInfo) - ) - ); - } - - async getAuthorizationInfo(authorizationCode: string, codeVerifier: string): Promise { - const url = globalState.config.oauth.authority + globalState.config.oauth.tokenEndpoint; - const { clientId, clientSecret } = globalState.config.oauth; - const s = new URLSearchParams([ - ['code', authorizationCode], - ['code_verifier', codeVerifier], - ['grant_type', 'authorization_code'], - ['client_id', clientId], - ['client_secret', clientSecret], - ['redirect_uri', this.listenUrl], - ]); - const res = await fetch(url, { - method: 'POST', - body: s, - headers: [['Content-Type', 'application/x-www-form-urlencoded']], - }); - if (res.status === 200) { - return Object.assign( - new UserAuthorizationInfo('', '', 0, ''), - convertObjectKeysToCamelCase((await res.json()) as object) - ); - } - throw Error(`Request access token failed, ${res.status}, ${res.statusText}, ${await res.text()}`); - } - - resolveAuthorizationCode(callbackUrl: string): string | undefined { - const splitted = callbackUrl.split('?'); - if (splitted.length < 2) return undefined; - - const s = new URLSearchParams(splitted[1]); - const code = s.get('code'); - return code ? code : undefined; - } - - private handleOAuthRedirect = async ( - req: express.Request, - res: express.Response - ): Promise => { - let authorizationInfo: UserAuthorizationInfo | undefined = undefined; - try { - const code = this.resolveAuthorizationCode(req.originalUrl); - if (!code) throw Error(`Unable to resolve authorization code from callback url, ${req.originalUrl}`); - - authorizationInfo = await this.getAuthorizationInfo(code, this._codeVerifier); - res.send( - `

授权成功, 您现在可以关闭此页面, 返回vscode.

- ` - ); - return authorizationInfo; - } catch (err) { - res.send('发生了错误!' + JSON.stringify(err, undefined, 4)); - return { error: err as object }; - } finally { - this._server?.close(); - } - }; -} diff --git a/src/services/edit-post-uri-handler.ts b/src/services/edit-post-uri-handler.ts deleted file mode 100644 index b2fc3380..00000000 --- a/src/services/edit-post-uri-handler.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ProviderResult, Uri, UriHandler } from 'vscode'; -import { openPostInVscode } from '../commands/posts-list/open-post-in-vscode'; - -export class EditPostUriHandler implements UriHandler { - handleUri(uri: Uri): ProviderResult { - const { path } = uri; - const splits = path.split('/'); - if (splits.length >= 3 && splits[1] === 'edit-post') { - const postId = parseInt(splits[2]); - if (postId > 0) openPostInVscode(postId).then(undefined, () => void 0); - } - } -} diff --git a/src/services/global-state.ts b/src/services/global-state.ts index 2df43c8b..7bf28699 100644 --- a/src/services/global-state.ts +++ b/src/services/global-state.ts @@ -1,19 +1,11 @@ -import { ExtensionContext, Uri } from 'vscode'; -import { defaultConfig, devConfig, IConfig, isDev } from '../models/config'; +import { env, ExtensionContext, Uri } from 'vscode'; +import { defaultConfig, devConfig, IExtensionConfig, isDev } from '../models/config'; import path from 'path'; -export class GlobalState { - private static _instance = new GlobalState(); - +class GlobalContext { private _extensionContext?: ExtensionContext; - private _config: IConfig = defaultConfig; - private _devConfig: IConfig = devConfig; - - protected constructor() {} - - static get instance() { - return this._instance; - } + private readonly _config: IExtensionConfig = defaultConfig; + private readonly _devConfig: IExtensionConfig = devConfig; get secretsStorage() { return this.extensionContext.secrets; @@ -23,7 +15,7 @@ export class GlobalState { return this.extensionContext.globalState; } - get config(): IConfig { + get config(): IExtensionConfig { return isDev() ? this._devConfig : this._config; } @@ -38,12 +30,25 @@ export class GlobalState { get extensionName(): string { const { name } = <{ name?: string }>this.extensionContext.extension.packageJSON; - return name ?? ''; + return name ?? 'vscode-cnb'; + } + + get publisher(): string { + const { publisher } = <{ publisher?: string }>this.extensionContext.extension.packageJSON; + return publisher ?? 'cnblogs'; + } + + get displayName() { + return this.extensionContext.extension.packageJSON.displayName as string; } get assetsUri() { - return Uri.file(path.join(globalState.extensionContext.extensionPath, 'dist', 'assets')); + return Uri.file(path.join(globalContext.extensionContext.extensionPath, 'dist', 'assets')); + } + + get extensionUrl() { + return `${env.uriScheme}://${this.publisher}.${this.extensionName}`; } } -export const globalState = GlobalState.instance; +export const globalContext = new GlobalContext(); diff --git a/src/services/image.service.ts b/src/services/image.service.ts index e771bc82..e967db8e 100644 --- a/src/services/image.service.ts +++ b/src/services/image.service.ts @@ -1,54 +1,45 @@ -import fetch from 'node-fetch'; -import FormData from 'form-data'; -import { accountService } from './account.service'; -import { globalState } from './global-state'; -import { throwIfNotOkResponse } from '../utils/throw-if-not-ok-response'; -import { Stream } from 'stream'; +import { globalContext } from './global-state'; +import { Readable } from 'stream'; import mime from 'mime'; +import { isString, merge, pick } from 'lodash-es'; +import FormData from 'form-data'; +import httpClient from '@/utils/http-client'; +import path from 'path'; -export class ImageService { - private static _instance: ImageService; - - private constructor() {} - - static get instance() { - if (!this._instance) this._instance = new ImageService(); - - return this._instance; - } - - async upload(file: T): Promise { +class ImageService { + async upload( + file: T + ): Promise { const form = new FormData(); - let { name: filename } = <{ name?: string }>file; - filename ??= 'image.png'; - form.append('image', file, { - filename, - contentType: 'image/png', - }); - const response = await fetch(`${globalState.config.apiBaseUrl}/api/posts/body/images`, { - method: 'POST', - headers: [accountService.buildBearerAuthorizationHeader()], + const { name, fileName, filename, path: _path } = file; + const finalName = path.basename(isString(_path) ? _path : fileName || filename || name || 'image.png'); + const ext = path.extname(finalName); + const mimeType = mime.lookup(ext, 'image/png'); + form.append('image', file, { filename: finalName, contentType: mimeType }); + const response = await httpClient.post(`${globalContext.config.apiBaseUrl}/api/posts/body/images`, { body: form, }); - await throwIfNotOkResponse(response); - return response.text(); + + return response.body; } - async download( - link: string, - fileNameWithoutExtension?: string - ): Promise { - const response = await fetch(link, { - method: 'get', + /** + * Download the image from web + * This will reject if failed to download + * @param url The url of the web image + * @param name The name that expected applied to the downloaded image + * @returns The {@link Readable} stream + */ + async download(url: string, name?: string): Promise { + const response = await httpClient.get(url, { responseType: 'buffer' }); + const contentType = response.headers['content-type'] ?? 'image/png'; + name = !name ? 'image' : name; + + return merge(Readable.from(response.body), { + ...pick(response, 'httpVersion', 'headers'), + path: `${name}.${mime.extension(contentType) ?? 'png'}`, }); - const contentType = response.headers.get('content-type') ?? 'image/png'; - fileNameWithoutExtension = !fileNameWithoutExtension ? 'image' : fileNameWithoutExtension; - return response.ok && response.body != null - ? Object.assign(Stream.Readable.from(await response.buffer()), { - path: fileNameWithoutExtension + '.' + (mime.extension(contentType) ?? ''), - }) - : [response.status, response.statusText, await response.text()]; } } -export const imageService = ImageService.instance; +export const imageService = new ImageService(); diff --git a/src/services/images-extractor.service.ts b/src/services/images-extractor.service.ts index 41076178..601514dd 100644 --- a/src/services/images-extractor.service.ts +++ b/src/services/images-extractor.service.ts @@ -3,8 +3,10 @@ import fs from 'fs'; import { Uri } from 'vscode'; import { imageService } from './image.service'; import { isErrorResponse } from '../models/error-response'; +import { isString, trimEnd } from 'lodash-es'; +import { Readable } from 'stream'; -export interface MarkdownImage { +export interface ImageInformation { link: string; symbol: string; alt: string; @@ -12,13 +14,13 @@ export interface MarkdownImage { index?: number; } -export type MarkdownImages = MarkdownImage[]; +export type ImageInformationArray = ImageInformation[]; const markdownImageRegex = /(!\[.*?\])\((.*?)( {0,}["'].*?['"])?\)/g; const cnblogsImageLinkRegex = /\.cnblogs\.com\//; interface ImageTypeFilter { - (image: MarkdownImage): boolean; + (image: ImageInformation): boolean; } const webImageFilter: ImageTypeFilter = image => /^(https?:)?\/\//.test(image.link); @@ -42,12 +44,12 @@ export class MarkdownImagesExtractor { private _status: 'pending' | 'extracting' | 'extracted' = 'pending'; private _errors: [symbol: string, message: string][] = []; - private _images: MarkdownImages | null | undefined = null; + private _images: ImageInformationArray | null | undefined = null; constructor( private markdown: string, private filePath: Uri, - public onProgress?: (index: number, images: MarkdownImages) => void + public onProgress?: (index: number, images: ImageInformationArray) => void ) {} get status() { @@ -57,7 +59,7 @@ export class MarkdownImagesExtractor { return this._errors; } - async extract(): Promise<[source: MarkdownImage, result: MarkdownImage | null][]> { + async extract(): Promise<[source: ImageInformation, result: ImageInformation | null][]> { this._status = 'extracting'; const sourceImages = this.findImages(); let idx = 0; @@ -68,7 +70,7 @@ export class MarkdownImagesExtractor { const imageFile = newImageLink ? newImageLink : await this.resolveImageFile(image); if (imageFile !== false) { try { - newImageLink = typeof imageFile === 'string' ? imageFile : await imageService.upload(imageFile); + newImageLink = isString(imageFile) ? imageFile : await imageService.upload(imageFile); } catch (ex) { this._errors.push([ image.symbol, @@ -78,10 +80,11 @@ export class MarkdownImagesExtractor { result.push([ image, newImageLink - ? Object.assign({}, image, { + ? { + ...image, link: newImageLink, symbol: `![${image.alt}](${newImageLink}${image.title})`, - } as MarkdownImage) + } : null, ]); } else { @@ -92,11 +95,11 @@ export class MarkdownImagesExtractor { return result; } - findImages(): MarkdownImage[] { + findImages(): ImageInformation[] { return ( this._images == null ? (this._images = Array.from(this.markdown.matchAll(markdownImageRegex)) - .map(g => ({ + .map(g => ({ link: g[2], symbol: g[0], alt: g[1].substring(2, g[1].length - 1), @@ -108,27 +111,25 @@ export class MarkdownImagesExtractor { ).filter(x => createImageTypeFilter(this.imageType).call(null, x)); } - private async resolveImageFile(image: MarkdownImage) { + private async resolveImageFile(image: ImageInformation) { const { link, symbol, alt, title } = image; if (webImageFilter(image)) { - const imageStream = await imageService.download(link, alt ?? title); - if (!(imageStream instanceof Array)) { - return imageStream; - } else { - this._errors.push([symbol, `无法下载网络图片, ${imageStream[0]} - ${imageStream[2]}`]); - return false; - } + const imageStream = await imageService + .download(link, alt ?? title) + .catch(reason => this._errors.push([symbol, trimEnd(`下载图片失败, ${reason}`, ', ')])); + return imageStream instanceof Readable ? imageStream : false; } else { - const triedPathed: string[] = []; + const triedPaths: string[] = []; const createReadStream = (file: string) => { - triedPathed.push(file); + triedPaths.push(file); return fs.existsSync(file) ? fs.createReadStream(file) : false; }; let stream = createReadStream(link); stream = stream === false ? createReadStream(path.resolve(path.dirname(this.filePath.fsPath), link)) : stream; - if (stream === false) this._errors.push([symbol, `本地图片文件不存在(${triedPathed.join(', ')})`]); + if (stream === false) + this._errors.push([symbol, `本地图片文件不存在(已搜索路径: ${triedPaths.join(', ')})`]); return stream; } diff --git a/src/services/ing.api.ts b/src/services/ing.api.ts index de34124a..85f4b5a5 100644 --- a/src/services/ing.api.ts +++ b/src/services/ing.api.ts @@ -1,17 +1,16 @@ import { Ing, IngComment, IngPublishModel, IngType } from '@/models/ing'; -import { accountService } from '@/services/account.service'; import { AlertService } from '@/services/alert.service'; -import { globalState } from '@/services/global-state'; -import fetch from 'node-fetch'; +import { globalContext } from '@/services/global-state'; +import fetch from '@/utils/fetch-client'; import { URLSearchParams } from 'url'; import { isArray, isNumber, isObject } from 'lodash-es'; export class IngApi { async publishIng(ing: IngPublishModel): Promise { - const resp = await fetch(`${globalState.config.cnblogsOpenApiUrl}/api/statuses`, { + const resp = await fetch(`${globalContext.config.cnblogsOpenApiUrl}/api/statuses`, { method: 'POST', body: JSON.stringify(ing), - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], }).catch(reason => void AlertService.warning(JSON.stringify(reason))); if (!resp || !resp.ok) AlertService.error(`闪存发布失败, ${resp?.statusText ?? ''} ${JSON.stringify((await resp?.text()) ?? '')}`); @@ -21,13 +20,13 @@ export class IngApi { async list({ pageIndex = 1, pageSize = 30, type = IngType.all } = {}): Promise { const resp = await fetch( - `${globalState.config.cnblogsOpenApiUrl}/api/statuses/@${type}?${new URLSearchParams({ + `${globalContext.config.cnblogsOpenApiUrl}/api/statuses/@${type}?${new URLSearchParams({ pageIndex: `${pageIndex}`, pageSize: `${pageSize}`, }).toString()}`, { method: 'GET', - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], } ).catch(reason => void AlertService.warning(JSON.stringify(reason))); if (!resp || !resp.ok) { @@ -54,9 +53,9 @@ export class IngApi { const arr = isNumber(ingIds) ? [ingIds] : ingIds; return Promise.all( arr.map(id => - fetch(`${globalState.config.cnblogsOpenApiUrl}/api/statuses/${id}/comments`, { + fetch(`${globalContext.config.cnblogsOpenApiUrl}/api/statuses/${id}/comments`, { method: 'GET', - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], }).then( resp => resp?.json().then(obj => [id, obj as IngComment[] | null | undefined] as const) ?? @@ -73,9 +72,9 @@ export class IngApi { } comment(ingId: number, data: { replyTo?: number; parentCommentId?: number; content: string }) { - return fetch(`${globalState.config.cnblogsOpenApiUrl}/api/statuses/${ingId}/comments`, { + return fetch(`${globalContext.config.cnblogsOpenApiUrl}/api/statuses/${ingId}/comments`, { method: 'POST', - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], body: JSON.stringify(data), }) .then(async resp => { diff --git a/src/services/ings-list-webview-provider.ts b/src/services/ings-list-webview-provider.ts index 47ed05ff..ec37fc9d 100644 --- a/src/services/ings-list-webview-provider.ts +++ b/src/services/ings-list-webview-provider.ts @@ -1,4 +1,4 @@ -import { globalState } from 'src/services/global-state'; +import { globalContext } from 'src/services/global-state'; import { CancellationToken, commands, @@ -20,7 +20,7 @@ import { CommentIngCommandHandler } from '@/commands/ing/comment-ing'; export class IngsListWebviewProvider implements WebviewViewProvider { private static _instance?: IngsListWebviewProvider; - readonly viewId = `${globalState.extensionName}.ings-list-webview`; + readonly viewId = `${globalContext.extensionName}.ings-list-webview`; private readonly _baseTitle = '闪存'; private _view?: WebviewView; @@ -59,7 +59,7 @@ export class IngsListWebviewProvider implements WebviewViewProvider { } private get assetsUri() { - return globalState.assetsUri; + return globalContext.assetsUri; } private get ingApi() { @@ -69,7 +69,7 @@ export class IngsListWebviewProvider implements WebviewViewProvider { static ensureRegistered() { if (!this._instance) { this._instance = new IngsListWebviewProvider(); - globalState.extensionContext.subscriptions.push( + globalContext.extensionContext.subscriptions.push( window.registerWebviewViewProvider(this._instance.viewId, this._instance) ); } @@ -158,7 +158,7 @@ export class IngsListWebviewProvider implements WebviewViewProvider { await commands .executeCommand( 'setContext', - `${globalState.extensionName}.ingsList.isRefreshing`, + `${globalContext.extensionName}.ingsList.isRefreshing`, value ? true : undefined ) .then(undefined, () => undefined); @@ -169,7 +169,7 @@ export class IngsListWebviewProvider implements WebviewViewProvider { await commands .executeCommand( 'setContext', - `${globalState.extensionName}.ingsList.pageIndex`, + `${globalContext.extensionName}.ingsList.pageIndex`, value > 0 ? value : undefined ) .then(undefined, () => undefined); diff --git a/src/services/local-draft.service.ts b/src/services/local-draft.service.ts index d415f80c..a5593c88 100644 --- a/src/services/local-draft.service.ts +++ b/src/services/local-draft.service.ts @@ -22,7 +22,6 @@ export class LocalDraft { } async readAllText(): Promise { - const binary = await workspace.fs.readFile(this.filePathUri); - return new TextDecoder().decode(binary); + return Buffer.from(await workspace.fs.readFile(this.filePathUri)).toString(); } } diff --git a/src/services/oauth.api.ts b/src/services/oauth.api.ts new file mode 100644 index 00000000..8c50f9dc --- /dev/null +++ b/src/services/oauth.api.ts @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { TokenInformation } from '@/models/token-information'; +import { CnblogsAccountInformation } from '@/authentication/account-information'; +import { convertObjectKeysToCamelCase } from '@/services/fetch-json-response-to-camel-case'; +import { globalContext } from '@/services/global-state'; +import fetch from '@/utils/fetch-client'; +import got from '@/utils/http-client'; +import { CancellationToken } from 'vscode'; +import { AbortController } from 'node-abort-controller'; +import { AuthorizationHeaderKey } from '@/utils/constants'; + +export type UserInformationSpec = Pick & { + readonly blog_id: string; + readonly account_id: string; + readonly picture: string; +}; + +export class OauthApi { + async fetchToken({ + codeVerifier, + authorizationCode, + cancellationToken, + }: { + codeVerifier: string; + authorizationCode: string; + cancellationToken?: CancellationToken; + }): Promise { + const abortControl = new AbortController(); + if (cancellationToken?.isCancellationRequested) abortControl.abort(); + cancellationToken?.onCancellationRequested(() => abortControl.abort()); + + const url = globalContext.config.oauth.authority + globalContext.config.oauth.tokenEndpoint; + const { clientId, clientSecret } = globalContext.config.oauth; + + const res = await got.post(url, { + form: { + code: authorizationCode, + code_verifier: codeVerifier, + grant_type: 'authorization_code', + client_id: clientId, + client_secret: clientSecret, + redirect_uri: globalContext.extensionUrl, + }, + responseType: 'json', + signal: abortControl.signal, + headers: { + [AuthorizationHeaderKey]: '', + }, + }); + if (res.statusCode === 200) return convertObjectKeysToCamelCase(res.body); + + throw Error( + `Failed to request token endpoint, ${res.statusCode}, ${res.statusMessage}, ${res.rawBody.toString()}` + ); + } + + async fetchUserInformation( + token: string, + { cancellationToken }: { cancellationToken?: CancellationToken | null } = {} + ): Promise { + const { authority, userInfoEndpoint } = globalContext.config.oauth; + const abortController = new AbortController(); + + if (cancellationToken?.isCancellationRequested) abortController.abort(); + const cancellationSubscribe = cancellationToken?.onCancellationRequested(() => abortController.abort()); + + const { body } = await got(`${authority}${userInfoEndpoint}`, { + method: 'GET', + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { Authorization: `Bearer ${token}` }, + signal: abortController.signal, + responseType: 'json', + }).finally(() => { + cancellationSubscribe?.dispose(); + }); + + return body; + } + + async revoke(accessToken: string): Promise { + const { clientId, revocationEndpoint, authority } = globalContext.config.oauth; + + const body = new URLSearchParams([ + ['client_id', clientId], + ['token', accessToken], + ['token_type_hint', 'access_token'], + ]); + const url = `${authority}${revocationEndpoint}`; + const res = await fetch(url, { + method: 'POST', + body: body, + headers: [['Content-Type', 'application/x-www-form-urlencoded']], + }); + return res.ok; + } +} diff --git a/src/services/parse-webview-html.ts b/src/services/parse-webview-html.ts index 2d9c0f6a..21b2afd8 100644 --- a/src/services/parse-webview-html.ts +++ b/src/services/parse-webview-html.ts @@ -1,9 +1,9 @@ import vscode from 'vscode'; -import { globalState } from 'src/services/global-state'; +import { globalContext } from 'src/services/global-state'; export type WebviewEntryName = 'ing' | 'post-configuration'; export const parseWebviewHtml = async (entry: WebviewEntryName, webview: vscode.Webview) => - (await vscode.workspace.fs.readFile(vscode.Uri.joinPath(globalState.assetsUri, 'ui', entry, 'index.html'))) + (await vscode.workspace.fs.readFile(vscode.Uri.joinPath(globalContext.assetsUri, 'ui', entry, 'index.html'))) .toString() - .replace(/@PWD/g, webview.asWebviewUri(globalState.assetsUri).toString()); + .replace(/@PWD/g, webview.asWebviewUri(globalContext.assetsUri).toString()); diff --git a/src/services/post-category.service.ts b/src/services/post-category.service.ts index bd782440..faea8726 100644 --- a/src/services/post-category.service.ts +++ b/src/services/post-category.service.ts @@ -1,7 +1,6 @@ -import fetch from 'node-fetch'; +import fetch from '@/utils/fetch-client'; import { PostCategories, PostCategory, PostCategoryAddDto } from '../models/post-category'; -import { accountService } from './account.service'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; export class PostCategoryService { private static _instance: PostCategoryService; @@ -27,9 +26,7 @@ export class PostCategoryService { async fetchCategories(forceRefresh = false): Promise { if (this._cached && !forceRefresh) return this._cached; - const res = await fetch(`${globalState.config.apiBaseUrl}/api/category/blog/1/edit`, { - headers: [accountService.buildBearerAuthorizationHeader()], - }); + const res = await fetch(`${globalContext.config.apiBaseUrl}/api/category/blog/1/edit`); if (!res.ok) throw Error(`Failed to fetch post categories\n${res.status}\n${await res.text()}`); const categories = await res.json(); @@ -38,19 +35,19 @@ export class PostCategoryService { } async newCategory(categoryAddDto: PostCategoryAddDto) { - const res = await fetch(`${globalState.config.apiBaseUrl}/api/category/blog/1`, { + const res = await fetch(`${globalContext.config.apiBaseUrl}/api/category/blog/1`, { method: 'POST', body: JSON.stringify(categoryAddDto), - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], }); if (!res.ok) throw Error(`${res.status}-${res.statusText}\n${await res.text()}`); } async updateCategory(category: PostCategory) { - const res = await fetch(`${globalState.config.apiBaseUrl}/api/category/blog/${category.categoryId}`, { + const res = await fetch(`${globalContext.config.apiBaseUrl}/api/category/blog/${category.categoryId}`, { method: 'PUT', body: JSON.stringify(category), - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], }); if (!res.ok) throw Error(`${res.status}-${res.statusText}\n${await res.text()}`); } @@ -58,9 +55,9 @@ export class PostCategoryService { async deleteCategory(categoryId: number) { if (categoryId <= 0) throw Error('Invalid param categoryId'); - const res = await fetch(`${globalState.config.apiBaseUrl}/api/category/blog/${categoryId}`, { + const res = await fetch(`${globalContext.config.apiBaseUrl}/api/category/blog/${categoryId}`, { method: 'DELETE', - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], + headers: [['Content-Type', 'application/json']], }); if (!res.ok) throw Error(`${res.status}-${res.statusText}\n${await res.text()}`); } diff --git a/src/services/post-configuration-panel.service.ts b/src/services/post-configuration-panel.service.ts index d20fd051..93295289 100644 --- a/src/services/post-configuration-panel.service.ts +++ b/src/services/post-configuration-panel.service.ts @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash-es'; import vscode, { Uri } from 'vscode'; import { Post } from '../models/post'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; import { postCategoryService } from './post-category.service'; import { siteCategoryService } from './site-category.service'; import { postTagService } from './post-tag.service'; @@ -26,7 +26,7 @@ export namespace postConfigurationPanel { beforeUpdate?: (postToUpdate: Post, panel: vscode.WebviewPanel) => Promise; } - const resourceRootUri = () => globalState.assetsUri; + const resourceRootUri = () => globalContext.assetsUri; const setHtml = async (webview: vscode.Webview): Promise => { webview.html = await parseWebviewHtml('post-configuration', webview); @@ -101,7 +101,7 @@ export namespace postConfigurationPanel { }); const { webview } = panel; await setHtml(webview); - panel.iconPath = Uri.joinPath(globalState.extensionContext.extensionUri, 'dist', 'assets', 'favicon.svg'); + panel.iconPath = Uri.joinPath(globalContext.extensionContext.extensionUri, 'dist', 'assets', 'favicon.svg'); panels.set(panelId, panel); return panel; }; diff --git a/src/services/post-file-map.ts b/src/services/post-file-map.ts index 64f56de4..14449f93 100644 --- a/src/services/post-file-map.ts +++ b/src/services/post-file-map.ts @@ -1,6 +1,6 @@ import { postCategoriesDataProvider } from '../tree-view-providers/post-categories-tree-data-provider'; import { postsDataProvider } from '../tree-view-providers/posts-data-provider'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; const validatePostFileMap = (map: PostFileMap) => map[0] >= 0 && !!map[1]; @@ -10,7 +10,7 @@ export class PostFileMapManager { static storageKey = 'postFileMaps'; private static get maps(): PostFileMap[] { - return globalState.storage.get(this.storageKey) ?? []; + return globalContext.storage.get(this.storageKey) ?? []; } static updateOrCreateMany(maps: PostFileMap[]): Promise; @@ -38,7 +38,7 @@ export class PostFileMapManager { if (exist) exist[1] = filePath; else maps.push([postId, filePath]); - await globalState.storage.update(this.storageKey, maps.filter(validatePostFileMap)); + await globalContext.storage.update(this.storageKey, maps.filter(validatePostFileMap)); if (emitEvent) { postsDataProvider.fireTreeDataChangedEvent(postId); postCategoriesDataProvider.onPostUpdated({ refreshPosts: false, postIds: [postId] }); diff --git a/src/services/post-tag.service.ts b/src/services/post-tag.service.ts index 5a47a65a..ff1c3dc7 100644 --- a/src/services/post-tag.service.ts +++ b/src/services/post-tag.service.ts @@ -1,7 +1,6 @@ -import fetch from 'node-fetch'; +import got from '@/utils/http-client'; import { PostTag } from '../models/post-tag'; -import { accountService } from './account.service'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; export class PostTagService { private static _instance: PostTagService; @@ -19,15 +18,16 @@ export class PostTagService { async fetchTags(forceRefresh = false): Promise { if (this._cachedTags && !forceRefresh) return this._cachedTags; - const response = await fetch(`${globalState.config.apiBaseUrl}/api/tags/list`, { - method: 'GET', - headers: [accountService.buildBearerAuthorizationHeader()], - }); - if (!response.ok) throw Error(`获取标签失败!\n${await response.text()}`); + const { + ok: isOk, + url, + method, + body, + } = await got.get(`${globalContext.config.apiBaseUrl}/api/tags/list`); + if (!isOk) throw Error(`Failed to ${method} ${url}`); - const data = await response.json(); - return Array.isArray(data) - ? data.map((x: PostTag) => Object.assign(new PostTag(), x)).filter(({ name: tagName }) => tagName) + return Array.isArray(body) + ? body.map((x: PostTag) => Object.assign(new PostTag(), x)).filter(({ name: tagName }) => tagName) : []; } } diff --git a/src/services/post-title-sanitizer.service.ts b/src/services/post-title-sanitizer.service.ts index ba402699..d71d9f32 100644 --- a/src/services/post-title-sanitizer.service.ts +++ b/src/services/post-title-sanitizer.service.ts @@ -1,7 +1,7 @@ import path from 'path'; import sanitize from 'sanitize-filename'; import { Post } from '../models/post'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; import { PostFileMapManager } from './post-file-map'; type InvalidPostFileNameMap = [postId: number, invalidName: string | undefined | null]; @@ -18,13 +18,13 @@ class InvalidPostTitleStore { store(map: InvalidPostFileNameMap): Thenable { const [postId, invalidName] = map; const key = buildStorageKey(postId); - if (invalidName) return globalState.storage.update(key, invalidName); - else return globalState.storage.update(key, undefined); + if (invalidName) return globalContext.storage.update(key, invalidName); + else return globalContext.storage.update(key, undefined); } get(postId: number): string | undefined { const key = buildStorageKey(postId); - return globalState.storage.get(key); + return globalContext.storage.get(key); } } diff --git a/src/services/post.service.ts b/src/services/post.service.ts index 43bffb16..26f4e217 100644 --- a/src/services/post.service.ts +++ b/src/services/post.service.ts @@ -1,9 +1,8 @@ -import fetch from 'node-fetch'; +import fetch from '@/utils/fetch-client'; import { Post } from '../models/post'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; import { PageModel } from '../models/page-model'; import { PostsListState } from '../models/posts-list-state'; -import { accountService } from './account.service'; import { PostEditDto } from '../models/post-edit-dto'; import { PostUpdatedResponse } from '../models/post-updated-response'; import { throwIfNotOkResponse } from '../utils/throw-if-not-ok-response'; @@ -11,6 +10,8 @@ import { IErrorResponse } from '../models/error-response'; import { AlertService } from './alert.service'; import { PostFileMapManager } from './post-file-map'; import { ZzkSearchResult } from '../models/zzk-search-result'; +import got from '@/utils/http-client'; +import { keys, merge, omit } from 'lodash-es'; const defaultPageSize = 30; let newPostTemplate: PostEditDto | undefined; @@ -21,7 +22,7 @@ export class PostService { protected constructor() {} protected get _baseUrl() { - return globalState.config.apiBaseUrl; + return globalContext.config.apiBaseUrl; } static get instance() { @@ -29,7 +30,7 @@ export class PostService { } get postsListState(): PostsListState | undefined { - return globalState.storage.get('postsListState'); + return globalContext.storage.get('postsListState'); } async fetchPostsList({ @@ -50,7 +51,6 @@ export class PostService { ['cid', categoryId != null && categoryId > 0 ? `${categoryId}` : ''], ]); const response = await fetch(`${this._baseUrl}/api/posts/list?${s.toString()}`, { - headers: [accountService.buildBearerAuthorizationHeader()], method: 'GET', }); if (!response.ok) throw Error(`request failed, ${response.status}, ${await response.text()}`); @@ -70,7 +70,6 @@ export class PostService { async fetchPostEditDto(postId: number, muteErrorNotification = false): Promise { const response = await fetch(`${this._baseUrl}/api/posts/${postId}`, { - headers: [accountService.buildBearerAuthorizationHeader()], method: 'GET', }); try { @@ -95,28 +94,29 @@ export class PostService { async deletePost(postId: number) { const res = await fetch(`${this._baseUrl}/api/posts/${postId}`, { method: 'DELETE', - headers: [accountService.buildBearerAuthorizationHeader()], }); if (!res.ok) throw Error(`删除博文失败!\n${res.status}\n${await res.text()}`); } async deletePosts(postIds: number[]) { - const searchParams = new URLSearchParams(postIds.map(id => ['postIds', `${id}`])); + const searchParams = new URLSearchParams(postIds.map<[string, string]>(id => ['postIds', `${id}`])); const res = await fetch(`${this._baseUrl}/api/bulk-operation/post?${searchParams.toString()}`, { method: 'DELETE', - headers: [accountService.buildBearerAuthorizationHeader()], }); if (!res.ok) throw Error(`删除博文失败!\n${res.status}\n${await res.text()}`); } async updatePost(post: Post): Promise { - const response = await fetch(`${this._baseUrl}/api/posts`, { - headers: [accountService.buildBearerAuthorizationHeader(), ['Content-Type', 'application/json']], - method: 'POST', - body: JSON.stringify(post), - }); - await throwIfNotOkResponse(response); - return Object.assign(new PostUpdatedResponse(), await response.json()); + const { + ok: isOk, + url, + method, + body, + statusCode, + statusMessage, + } = await got.post(`${this._baseUrl}/api/posts`, { json: post, responseType: 'json' }); + if (!isOk) throw new Error(`Failed to ${method} ${url}, ${statusCode} - ${statusMessage}`); + return PostUpdatedResponse.parse(body); } async updatePostsListState(state: PostsListState | undefined | PageModel) { @@ -133,10 +133,10 @@ export class PostService { pageCount: state.pageCount, } : state; - await globalState.storage.update('postsListState', finalState); + await globalContext.storage.update('postsListState', finalState); } - async fetchPostEditDtoTemplate(): Promise { + async fetchPostEditTemplate(): Promise { if (!newPostTemplate) newPostTemplate = await this.fetchPostEditDto(-1); return newPostTemplate diff --git a/src/services/site-category.service.ts b/src/services/site-category.service.ts index 9b2c24fd..46e660c3 100644 --- a/src/services/site-category.service.ts +++ b/src/services/site-category.service.ts @@ -1,7 +1,6 @@ -import fetch from 'node-fetch'; +import fetch from '@/utils/fetch-client'; import { SiteCategories, SiteCategory } from '../models/site-category'; -import { accountService } from './account.service'; -import { globalState } from './global-state'; +import { globalContext } from './global-state'; export namespace siteCategoryService { let cached: SiteCategories | undefined; @@ -9,9 +8,7 @@ export namespace siteCategoryService { export const fetchAll = async (forceRefresh = false): Promise => { if (cached && !forceRefresh) return cached; - const response = await fetch(`${globalState.config.apiBaseUrl}/api/category/site`, { - headers: [accountService.buildBearerAuthorizationHeader()], - }); + const response = await fetch(`${globalContext.config.apiBaseUrl}/api/category/site`); if (!response.ok) throw Error(`Failed to fetch post categories\n${response.status}\n${await response.text()}`); const categories = await response.json(); diff --git a/src/tree-view-providers/account-view-data-provider.ts b/src/tree-view-providers/account-view-data-provider.ts index 27a3297e..b152d9c3 100644 --- a/src/tree-view-providers/account-view-data-provider.ts +++ b/src/tree-view-providers/account-view-data-provider.ts @@ -1,4 +1,4 @@ -import { accountService } from '../services/account.service'; +import { accountManager } from '../authentication/account-manager'; import { EventEmitter, ProviderResult, ThemeIcon, TreeDataProvider, TreeItem } from 'vscode'; export class AccountViewDataProvider implements TreeDataProvider { @@ -22,9 +22,9 @@ export class AccountViewDataProvider implements TreeDataProvider { } getChildren(element?: TreeItem): ProviderResult { - if (!accountService.isAuthorized || element) return []; + if (!accountManager.isAuthorized || element) return []; - const u = accountService.curUser; + const u = accountManager.curUser; return [ { label: u.name, tooltip: '用户名', iconPath: new ThemeIcon('account') }, { diff --git a/src/tree-view-providers/converters.ts b/src/tree-view-providers/converters.ts index 3c8555cd..2f5fa168 100644 --- a/src/tree-view-providers/converters.ts +++ b/src/tree-view-providers/converters.ts @@ -3,7 +3,7 @@ import { homedir } from 'os'; import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Post } from '../models/post'; import { PostCategory } from '../models/post-category'; -import { globalState } from '../services/global-state'; +import { globalContext } from '../services/global-state'; import { PostFileMapManager } from '../services/post-file-map'; import { Settings } from '../services/settings.service'; import { BaseTreeItemSource } from './models/base-tree-item-source'; @@ -41,7 +41,7 @@ const postConverter: Converter = obj => { return Object.assign(new TreeItem(`${obj.title}`, TreeItemCollapsibleState.Collapsed), { tooltip: new MarkdownString(`[${url}](${url})` + descDatePublished + descLocalPath), command: { - command: `${globalState.extensionName}.edit-post`, + command: `${globalContext.extensionName}.edit-post`, arguments: [obj.id], title: '编辑博文', }, diff --git a/src/tree-view-providers/post-categories-tree-data-provider.ts b/src/tree-view-providers/post-categories-tree-data-provider.ts index 452c549b..4872ee19 100644 --- a/src/tree-view-providers/post-categories-tree-data-provider.ts +++ b/src/tree-view-providers/post-categories-tree-data-provider.ts @@ -1,7 +1,7 @@ import { flattenDepth, take } from 'lodash-es'; import { commands, EventEmitter, MessageOptions, ProviderResult, TreeDataProvider, TreeItem, window } from 'vscode'; import { PostCategories } from '../models/post-category'; -import { globalState } from '../services/global-state'; +import { globalContext } from '../services/global-state'; import { postCategoryService } from '../services/post-category.service'; import { postService } from '../services/post.service'; import { toTreeItem } from './converters'; @@ -45,7 +45,7 @@ export class PostCategoriesTreeDataProvider implements TreeDataProvider { const disposables: IDisposable[] = []; for (const [, item] of Object.entries(_views)) typeof item === 'function' ? undefined : disposables.push(item); - globalState.extensionContext.subscriptions.push(...disposables); + globalContext.extensionContext.subscriptions.push(...disposables); return extensionViews; }; diff --git a/src/utils/check-access-token-expired.ts b/src/utils/check-access-token-expired.ts deleted file mode 100644 index 0e5f7c15..00000000 --- a/src/utils/check-access-token-expired.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const checkIsAccessTokenExpired = (accessToken: string): boolean => { - const decodedText = Buffer.from(accessToken.split('.')[1] ?? '', 'base64').toString(); - const { exp } = JSON.parse(decodedText) as { exp?: number }; - return typeof exp === 'number' ? exp * 1000 <= Date.now() : true; -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 00000000..8d2ea194 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1 @@ +export const AuthorizationHeaderKey = 'Authorization'; diff --git a/src/utils/fetch-client.ts b/src/utils/fetch-client.ts new file mode 100644 index 00000000..deb1d455 --- /dev/null +++ b/src/utils/fetch-client.ts @@ -0,0 +1,7 @@ +import httpClient from '@/utils/http-client'; +import { createFetch } from 'got-fetch'; + +const fetch = createFetch(httpClient); + +export * from 'got-fetch'; +export default fetch; diff --git a/src/utils/get-clipboard-image.ts b/src/utils/get-clipboard-image.ts index 52368434..26e8fa4a 100644 --- a/src/utils/get-clipboard-image.ts +++ b/src/utils/get-clipboard-image.ts @@ -5,7 +5,7 @@ import path from 'path'; import fs from 'fs'; import os from 'os'; import isWsl from 'is-wsl'; -import { globalState } from '../services/global-state'; +import { globalContext } from '../services/global-state'; import { AlertService } from '../services/alert.service'; import { IClipboardImage } from '../models/clipboard-image'; import { format } from 'date-fns'; @@ -30,8 +30,8 @@ const getCurrentPlatform = (): Platform => { const readClipboardScript = ( scriptName: 'mac.applescript' | 'linux.sh' | 'windows.ps1' | 'windows10.ps1' | 'wsl.sh' ) => { - const filePath = globalState.extensionContext.asAbsolutePath(`dist/assets/scripts/clipboard/${scriptName}`); - return new TextDecoder().decode(fs.readFileSync(filePath)); + const filePath = globalContext.extensionContext.asAbsolutePath(`dist/assets/scripts/clipboard/${scriptName}`); + return fs.readFileSync(filePath).toString(); }; const platform2ScriptContent = (): { @@ -59,7 +59,7 @@ const platform2ScriptFilename: { const getClipboardImage = (): Promise => { const imagePath = path.join( - globalState.extensionContext?.asAbsolutePath('./') ?? '', + globalContext.extensionContext?.asAbsolutePath('./') ?? '', `${format(new Date(), 'yyyyMMddHHmmss')}.png` ); return new Promise((resolve, reject): void => { diff --git a/src/utils/http-client.ts b/src/utils/http-client.ts new file mode 100644 index 00000000..de2bfbec --- /dev/null +++ b/src/utils/http-client.ts @@ -0,0 +1,25 @@ +import { accountManager } from '@/authentication/account-manager'; +import { AuthorizationHeaderKey } from '@/utils/constants'; +import got, { BeforeRequestHook } from 'got'; +import { isString } from 'lodash-es'; + +const bearerTokenHook: BeforeRequestHook = async opt => { + const { headers } = opt; + + if (Object.keys(headers).findIndex(x => x.toLowerCase() === AuthorizationHeaderKey.toLowerCase()) < 0) { + const token = await accountManager.acquireToken().catch((reason: unknown) => ({ reason })); + if (isString(token)) headers[AuthorizationHeaderKey] = `Bearer ${token}`; + } +}; + +const httpClient = got.extend({ + hooks: { + beforeRequest: [bearerTokenHook], + }, + throwHttpErrors: true, + https: { rejectUnauthorized: false }, +}); + +export { got }; +export * from 'got'; +export default httpClient; diff --git a/src/utils/throw-if-not-ok-response.ts b/src/utils/throw-if-not-ok-response.ts index 37202327..591bae03 100644 --- a/src/utils/throw-if-not-ok-response.ts +++ b/src/utils/throw-if-not-ok-response.ts @@ -1,7 +1,7 @@ -import { Response } from 'node-fetch'; +import { GotFetchResponse } from 'got-fetch/out/lib/response'; import { IErrorResponse, isErrorResponse } from '../models/error-response'; -const throwIfNotOkResponse = async (response: Response) => { +const throwIfNotOkResponse = async (response: GotFetchResponse) => { if (!response.ok) { const responseText = await response.text(); let responseJson: unknown; diff --git a/src/utils/typed-keys.ts b/src/utils/typed-keys.ts new file mode 100644 index 00000000..d04b57e9 --- /dev/null +++ b/src/utils/typed-keys.ts @@ -0,0 +1,3 @@ +import { keys } from 'lodash-es'; + +export const typedKeys = (obj: T) => keys(obj) as (keyof T)[]; diff --git a/src/utils/uri-handler.ts b/src/utils/uri-handler.ts new file mode 100644 index 00000000..cb2d8a28 --- /dev/null +++ b/src/utils/uri-handler.ts @@ -0,0 +1,41 @@ +import { Disposable, EventEmitter, ProviderResult, Uri, UriHandler, Event } from 'vscode'; +import { openPostInVscode } from '../commands/posts-list/open-post-in-vscode'; + +class ExtensionUriHandler implements UriHandler, Disposable { + private _uriEventEmitter?: EventEmitter; + private readonly _disposable: Disposable; + private _onUri?: Event; + + constructor() { + this._disposable = Disposable.from( + this.onUri(uri => { + const { path } = uri; + const splits = path.split('/'); + if (splits.length >= 3 && splits[1] === 'edit-post') { + const postId = parseInt(splits[2]); + if (postId > 0) openPostInVscode(postId).then(undefined, () => void 0); + } + }) + ); + } + + private get uriEventEmitter() { + return (this._uriEventEmitter ??= new EventEmitter()); + } + + get onUri() { + return (this._onUri ??= this.uriEventEmitter.event); + } + + handleUri(uri: Uri): ProviderResult { + this._uriEventEmitter?.fire(uri); + } + + dispose() { + this._disposable.dispose(); + } +} + +const extensionUriHandler = new ExtensionUriHandler(); + +export default extensionUriHandler; diff --git a/ui/ing/App.tsx b/ui/ing/App.tsx index a4a0b3bf..66026d89 100644 --- a/ui/ing/App.tsx +++ b/ui/ing/App.tsx @@ -7,7 +7,7 @@ import { Ing, IngComment } from '@models/ing'; import { activeThemeProvider } from 'share/active-theme-provider'; import { ThemeProvider } from '@fluentui/react/lib/Theme'; import { Spinner, Stack } from '@fluentui/react'; -import { cloneWith } from 'lodash'; +import { cloneWith } from 'lodash-es'; export class App extends Component { constructor(props: unknown) {