mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-10-24 23:01:12 +00:00
Merge d6bfef3e4a into d505845f91
This commit is contained in:
commit
538b2a2a7c
9 changed files with 915 additions and 191 deletions
43
.github/workflows/e2e-tests.yml
vendored
43
.github/workflows/e2e-tests.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
- main
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 90
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
@ -23,10 +23,10 @@ jobs:
|
|||
|
||||
- name: Get Playwright version
|
||||
id: playwright-version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT"
|
||||
run: echo "PLAYWRIGHT_VERSION=$(jq -r '.devDependencies["@playwright/test"]' package.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: pnpm install --ignore-scripts
|
||||
|
||||
- name: Build app
|
||||
run: pnpm build
|
||||
|
|
@ -40,8 +40,41 @@ jobs:
|
|||
${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
# --with-deps bundles an apt-get run for the full browser dependency
|
||||
# set; the ubuntu archive is intermittently very slow, so it gets its
|
||||
# own step with a generous timeout, retries, and an apt-cache entry so
|
||||
# a one-time successful run seeds later ones
|
||||
- name: Restore apt cache for Playwright dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/apt-playwright-cache
|
||||
key: ${{ runner.os }}-apt-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
|
||||
|
||||
- name: Install Playwright system dependencies
|
||||
timeout-minutes: 45
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if timeout 1500 ./node_modules/.bin/playwright install-deps; then
|
||||
exit 0
|
||||
fi
|
||||
echo "playwright install-deps attempt $attempt failed (attempt timeout 25 min), retrying in 20s"
|
||||
sleep 20
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: Install Playwright browsers
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT: 60000
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
if ./node_modules/.bin/playwright install; then
|
||||
exit 0
|
||||
fi
|
||||
echo "playwright install attempt $attempt failed, retrying in 15s"
|
||||
sleep 15
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm run test:e2e --shard=${{ matrix.shard }}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
"@antfu/eslint-config": "^0.41.0",
|
||||
"@iconify-json/mdi": "^1.1.50",
|
||||
"@intlify/unplugin-vue-i18n": "^2.0.0",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
|
|
@ -149,5 +149,10 @@
|
|||
"vitest": "^0.34.0",
|
||||
"workbox-window": "^7.0.0",
|
||||
"zx": "^7.2.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@vueuse/shared": "10.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
260
pnpm-lock.yaml
generated
260
pnpm-lock.yaml
generated
|
|
@ -4,6 +4,9 @@ settings:
|
|||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
'@vueuse/shared': 10.3.0
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
|
|
@ -49,7 +52,7 @@ importers:
|
|||
version: 10.3.0(vue@3.3.4)
|
||||
'@vueuse/head':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(typescript@5.2.2)(vue@3.3.4)
|
||||
version: 1.0.0(vue@3.3.4)
|
||||
'@vueuse/router':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4)
|
||||
|
|
@ -220,8 +223,8 @@ importers:
|
|||
specifier: ^2.0.0
|
||||
version: 2.0.0(rollup@2.79.2)(vue-i18n@9.9.1(vue@3.3.4))
|
||||
'@playwright/test':
|
||||
specifier: ^1.32.3
|
||||
version: 1.32.3
|
||||
specifier: ^1.62.1
|
||||
version: 1.63.0
|
||||
'@rushstack/eslint-patch':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
|
|
@ -332,7 +335,7 @@ importers:
|
|||
version: 4.0.0
|
||||
vitest:
|
||||
specifier: ^0.34.0
|
||||
version: 0.34.0(jsdom@22.0.0)(less@4.1.3)(terser@5.37.0)
|
||||
version: 0.34.0(jsdom@22.0.0)(less@4.1.3)(playwright@1.63.0)(terser@5.37.0)
|
||||
workbox-window:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
|
|
@ -356,17 +359,20 @@ packages:
|
|||
|
||||
'@antfu/eslint-config-basic@0.41.0':
|
||||
resolution: {integrity: sha512-zcwFv+nEV/NroeeVWriNdnIGd9soOLRG8wIiVz4VVJ0BjONrqQR56HLG/gDxH/1GUYBnQCEcVxGUmegce08cnw==}
|
||||
deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config
|
||||
peerDependencies:
|
||||
eslint: '>=7.4.0'
|
||||
|
||||
'@antfu/eslint-config-ts@0.41.0':
|
||||
resolution: {integrity: sha512-ng3GYpJGZgrxGwBVda/MgUpveH3LbEqdPCFi1+S5e62W4kf8rmEVbhc0I8w7/aKN0uNqir5SInYg8gob2maDAQ==}
|
||||
deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config
|
||||
peerDependencies:
|
||||
eslint: '>=7.4.0'
|
||||
typescript: '>=3.9'
|
||||
|
||||
'@antfu/eslint-config-vue@0.41.0':
|
||||
resolution: {integrity: sha512-iJiEGRUgRmT3mQCmGl0hTMwq/ShXRjRPjpgsDcphKJyEF06ZIR/4gxHn+utQRLT2hD39DQN8gk0ZbpV3gWtf/g==}
|
||||
deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config
|
||||
peerDependencies:
|
||||
eslint: '>=7.4.0'
|
||||
|
||||
|
|
@ -1464,6 +1470,7 @@ packages:
|
|||
'@humanwhocodes/config-array@0.11.10':
|
||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
deprecated: Use @eslint/config-array instead
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
|
|
@ -1471,6 +1478,7 @@ packages:
|
|||
|
||||
'@humanwhocodes/object-schema@1.2.1':
|
||||
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@iconify-json/mdi@1.1.50':
|
||||
resolution: {integrity: sha512-SgbT5w5eHCdOG74ZWPz7HlTGk6VsifIJhNi6lAsxj/5Nlqt6Cz4LlQmSa9eecU9p075Jub2aAx/o7YI+GCahRQ==}
|
||||
|
|
@ -1626,9 +1634,9 @@ packages:
|
|||
resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/test@1.32.3':
|
||||
resolution: {integrity: sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==}
|
||||
engines: {node: '>=14'}
|
||||
'@playwright/test@1.63.0':
|
||||
resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.28':
|
||||
|
|
@ -2303,9 +2311,6 @@ packages:
|
|||
'@vue/compiler-core@3.3.7':
|
||||
resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==}
|
||||
|
||||
'@vue/compiler-core@3.5.13':
|
||||
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
|
||||
|
||||
'@vue/compiler-dom@3.2.47':
|
||||
resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
|
||||
|
||||
|
|
@ -2315,18 +2320,12 @@ packages:
|
|||
'@vue/compiler-dom@3.3.7':
|
||||
resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==}
|
||||
|
||||
'@vue/compiler-dom@3.5.13':
|
||||
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
|
||||
|
||||
'@vue/compiler-sfc@3.2.47':
|
||||
resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
|
||||
|
||||
'@vue/compiler-sfc@3.3.4':
|
||||
resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
|
||||
|
||||
'@vue/compiler-sfc@3.5.13':
|
||||
resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
|
||||
|
||||
'@vue/compiler-ssr@3.2.47':
|
||||
resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
|
||||
|
||||
|
|
@ -2336,9 +2335,6 @@ packages:
|
|||
'@vue/compiler-ssr@3.3.7':
|
||||
resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==}
|
||||
|
||||
'@vue/compiler-ssr@3.5.13':
|
||||
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
|
||||
|
||||
'@vue/devtools-api@6.5.0':
|
||||
resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
|
||||
|
||||
|
|
@ -2359,21 +2355,12 @@ packages:
|
|||
'@vue/reactivity@3.3.4':
|
||||
resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
|
||||
|
||||
'@vue/reactivity@3.5.13':
|
||||
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
|
||||
|
||||
'@vue/runtime-core@3.3.4':
|
||||
resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==}
|
||||
|
||||
'@vue/runtime-core@3.5.13':
|
||||
resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
|
||||
|
||||
'@vue/runtime-dom@3.3.4':
|
||||
resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==}
|
||||
|
||||
'@vue/runtime-dom@3.5.13':
|
||||
resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
|
||||
|
||||
'@vue/server-renderer@3.3.4':
|
||||
resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -2384,11 +2371,6 @@ packages:
|
|||
peerDependencies:
|
||||
vue: 3.3.7
|
||||
|
||||
'@vue/server-renderer@3.5.13':
|
||||
resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
|
||||
peerDependencies:
|
||||
vue: 3.5.13
|
||||
|
||||
'@vue/shared@3.2.47':
|
||||
resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
|
||||
|
||||
|
|
@ -2398,9 +2380,6 @@ packages:
|
|||
'@vue/shared@3.3.7':
|
||||
resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==}
|
||||
|
||||
'@vue/shared@3.5.13':
|
||||
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
||||
|
||||
'@vue/test-utils@2.3.2':
|
||||
resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==}
|
||||
peerDependencies:
|
||||
|
|
@ -2428,20 +2407,15 @@ packages:
|
|||
peerDependencies:
|
||||
vue-router: '>=4.0.0-rc.1'
|
||||
|
||||
'@vueuse/shared@10.0.0':
|
||||
resolution: {integrity: sha512-Zh3LgJqvUBWVY3SiMvXanTcfAneGbt63QPczBRDNgQ6jd/ehodO9a1lCFzaA6SWJJoI+ugVTjHFYJdoR656DVQ==}
|
||||
|
||||
'@vueuse/shared@10.3.0':
|
||||
resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
|
||||
|
||||
'@vueuse/shared@12.0.0':
|
||||
resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==}
|
||||
|
||||
'@zhead/schema@1.0.0-beta.13':
|
||||
resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==}
|
||||
|
||||
abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
|
@ -2552,6 +2526,7 @@ packages:
|
|||
|
||||
babel-merge@3.0.0:
|
||||
resolution: {integrity: sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
||||
|
|
@ -2798,7 +2773,7 @@ packages:
|
|||
hasBin: true
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
confbox@0.1.8:
|
||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||
|
|
@ -2850,6 +2825,7 @@ packages:
|
|||
|
||||
crypto-js@4.1.1:
|
||||
resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==}
|
||||
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
|
||||
|
||||
crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
|
|
@ -2896,9 +2872,6 @@ packages:
|
|||
csstype@3.1.2:
|
||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
dash-get@1.0.2:
|
||||
resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==}
|
||||
|
||||
|
|
@ -3054,6 +3027,7 @@ packages:
|
|||
domexception@4.0.0:
|
||||
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
|
|
@ -3246,6 +3220,7 @@ packages:
|
|||
eslint-plugin-i@2.28.0-2:
|
||||
resolution: {integrity: sha512-z48kG4qmE4TmiLcxbmvxMT5ycwvPkXaWW0XpU1L768uZaTbiDbxsHMEdV24JHlOR1xDsPpKW39BfP/pRdYIwFA==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead
|
||||
peerDependencies:
|
||||
eslint: ^7.2.0 || ^8
|
||||
|
||||
|
|
@ -3271,6 +3246,7 @@ packages:
|
|||
eslint-plugin-markdown@3.0.1:
|
||||
resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
deprecated: Please use @eslint/markdown instead
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
|
|
@ -3337,6 +3313,7 @@ packages:
|
|||
eslint@8.47.0:
|
||||
resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||
hasBin: true
|
||||
|
||||
espree@9.6.1:
|
||||
|
|
@ -3499,11 +3476,6 @@ packages:
|
|||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
|
@ -3566,10 +3538,12 @@ packages:
|
|||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
|
|
@ -3758,6 +3732,7 @@ packages:
|
|||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
|
@ -4172,6 +4147,7 @@ packages:
|
|||
|
||||
loupe@2.3.6:
|
||||
resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==}
|
||||
deprecated: Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5
|
||||
|
||||
lower-case-first@1.0.2:
|
||||
resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==}
|
||||
|
|
@ -4380,6 +4356,7 @@ packages:
|
|||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.4:
|
||||
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
||||
|
|
@ -4610,10 +4587,16 @@ packages:
|
|||
plausible-tracker@0.3.8:
|
||||
resolution: {integrity: sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
playwright-core@1.32.3:
|
||||
resolution: {integrity: sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==}
|
||||
engines: {node: '>=14'}
|
||||
playwright-core@1.63.0:
|
||||
resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.63.0:
|
||||
resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
pluralize@8.0.0:
|
||||
|
|
@ -4868,10 +4851,6 @@ packages:
|
|||
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==}
|
||||
hasBin: true
|
||||
|
||||
resolve@1.22.8:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
|
||||
resolve@1.22.9:
|
||||
resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==}
|
||||
hasBin: true
|
||||
|
|
@ -4894,6 +4873,7 @@ packages:
|
|||
|
||||
rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rollup@2.79.2:
|
||||
|
|
@ -5071,9 +5051,11 @@ packages:
|
|||
source-map@0.8.0-beta.0:
|
||||
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
||||
engines: {node: '>= 8'}
|
||||
deprecated: The work that was done in this beta branch won't be included in future versions
|
||||
|
||||
sourcemap-codec@1.4.8:
|
||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||
deprecated: Please use @jridgewell/sourcemap-codec instead
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||
|
|
@ -5544,6 +5526,7 @@ packages:
|
|||
|
||||
uuid@9.0.0:
|
||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
|
|
@ -5569,6 +5552,7 @@ packages:
|
|||
|
||||
vite-plugin-vue-markdown@0.23.5:
|
||||
resolution: {integrity: sha512-NXTZ4y+n691gLPWayMBbh4jldQeaqDp9e9WjWUYbn9obsLqS9qU+hr4RAruDq5kP4siTOp7JDV34Sw5eA7WxLg==}
|
||||
deprecated: '`vite-plugin-vue-markdown` is renamed to `unplugin-vue-markdown`. For usages in Vite, you also need to change the import path to `unplugin-vue-markdown/vite`.'
|
||||
peerDependencies:
|
||||
vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0
|
||||
|
||||
|
|
@ -5675,6 +5659,7 @@ packages:
|
|||
vue-i18n@9.9.1:
|
||||
resolution: {integrity: sha512-xyQ4VspLdNSPTKBFBPWa1tvtj+9HuockZwgFeD2OhxxXuC2CWeNvV4seu2o9+vbQOyQbhAM5Ez56oxUrrnTWdw==}
|
||||
engines: {node: '>= 16'}
|
||||
deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
|
|
@ -5701,14 +5686,6 @@ packages:
|
|||
vue@3.3.4:
|
||||
resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
|
||||
|
||||
vue@3.5.13:
|
||||
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
vuedraggable@4.1.0:
|
||||
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
|
||||
peerDependencies:
|
||||
|
|
@ -5754,6 +5731,7 @@ packages:
|
|||
whatwg-encoding@2.0.0:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-mimetype@3.0.0:
|
||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||
|
|
@ -6117,7 +6095,7 @@ snapshots:
|
|||
'@babel/traverse': 7.23.2
|
||||
'@babel/types': 7.23.0
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
|
|
@ -7030,7 +7008,7 @@ snapshots:
|
|||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/parser': 7.23.0
|
||||
'@babel/types': 7.23.0
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -7401,7 +7379,7 @@ snapshots:
|
|||
|
||||
'@linaria/logger@4.0.0':
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
picocolors: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -7462,12 +7440,9 @@ snapshots:
|
|||
tiny-glob: 0.2.9
|
||||
tslib: 2.5.0
|
||||
|
||||
'@playwright/test@1.32.3':
|
||||
'@playwright/test@1.63.0':
|
||||
dependencies:
|
||||
'@types/node': 18.15.11
|
||||
playwright-core: 1.32.3
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
playwright: 1.63.0
|
||||
|
||||
'@polka/url@1.0.0-next.28': {}
|
||||
|
||||
|
|
@ -7922,7 +7897,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2)
|
||||
'@typescript-eslint/utils': 6.4.1(eslint@8.47.0)(typescript@5.2.2)
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
eslint: 8.47.0
|
||||
ts-api-utils: 1.0.1(typescript@5.2.2)
|
||||
optionalDependencies:
|
||||
|
|
@ -7940,10 +7915,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 5.60.0
|
||||
'@typescript-eslint/visitor-keys': 5.60.0
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
tsutils: 3.21.0(typescript@5.2.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.2.2
|
||||
|
|
@ -7954,10 +7929,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 6.4.1
|
||||
'@typescript-eslint/visitor-keys': 6.4.1
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
ts-api-utils: 1.0.1(typescript@5.2.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.2.2
|
||||
|
|
@ -7968,10 +7943,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 6.9.1
|
||||
'@typescript-eslint/visitor-keys': 6.9.1
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
ts-api-utils: 1.0.1(typescript@5.2.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.2.2
|
||||
|
|
@ -7988,7 +7963,7 @@ snapshots:
|
|||
'@typescript-eslint/typescript-estree': 5.60.0(typescript@5.2.2)
|
||||
eslint: 8.47.0
|
||||
eslint-scope: 5.1.1
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
|
@ -8002,7 +7977,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 6.4.1
|
||||
'@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2)
|
||||
eslint: 8.47.0
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
|
@ -8049,15 +8024,15 @@ snapshots:
|
|||
dependencies:
|
||||
'@unhead/schema': 0.5.1
|
||||
|
||||
'@unhead/vue@0.5.1(typescript@5.2.2)(vue@3.3.4)':
|
||||
'@unhead/vue@0.5.1(vue@3.3.4)':
|
||||
dependencies:
|
||||
'@unhead/dom': 0.5.1
|
||||
'@unhead/schema': 0.5.1
|
||||
'@vueuse/shared': 12.0.0(typescript@5.2.2)
|
||||
'@vueuse/shared': 10.3.0(vue@3.3.4)
|
||||
unhead: 0.5.1
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- '@vue/composition-api'
|
||||
|
||||
'@unocss/astro@0.65.1(rollup@2.79.2)(vite@4.4.9(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0))(vue@3.3.4)':
|
||||
dependencies:
|
||||
|
|
@ -8341,14 +8316,6 @@ snapshots:
|
|||
source-map-js: 1.0.2
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-core@3.5.13':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
'@vue/shared': 3.5.13
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-dom@3.2.47':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.2.47
|
||||
|
|
@ -8365,11 +8332,6 @@ snapshots:
|
|||
'@vue/shared': 3.3.7
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-dom@3.5.13':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
|
||||
'@vue/compiler-sfc@3.2.47':
|
||||
dependencies:
|
||||
'@babel/parser': 7.21.4
|
||||
|
|
@ -8396,18 +8358,6 @@ snapshots:
|
|||
postcss: 8.4.28
|
||||
source-map-js: 1.0.2
|
||||
|
||||
'@vue/compiler-sfc@3.5.13':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
'@vue/compiler-core': 3.5.13
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
'@vue/compiler-ssr': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.15
|
||||
postcss: 8.4.49
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-ssr@3.2.47':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.2.47
|
||||
|
|
@ -8424,11 +8374,6 @@ snapshots:
|
|||
'@vue/shared': 3.3.7
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-ssr@3.5.13':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
|
||||
'@vue/devtools-api@6.5.0': {}
|
||||
|
||||
'@vue/language-core@1.8.1(typescript@5.2.2)':
|
||||
|
|
@ -8464,33 +8409,17 @@ snapshots:
|
|||
dependencies:
|
||||
'@vue/shared': 3.3.4
|
||||
|
||||
'@vue/reactivity@3.5.13':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.13
|
||||
|
||||
'@vue/runtime-core@3.3.4':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
|
||||
'@vue/runtime-core@3.5.13':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
|
||||
'@vue/runtime-dom@3.3.4':
|
||||
dependencies:
|
||||
'@vue/runtime-core': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
csstype: 3.1.2
|
||||
|
||||
'@vue/runtime-dom@3.5.13':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.13
|
||||
'@vue/runtime-core': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vue/server-renderer@3.3.4(vue@3.3.4)':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.3.4
|
||||
|
|
@ -8504,12 +8433,6 @@ snapshots:
|
|||
vue: 3.3.4
|
||||
optional: true
|
||||
|
||||
'@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.2.2))':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
vue: 3.5.13(typescript@5.2.2)
|
||||
|
||||
'@vue/shared@3.2.47': {}
|
||||
|
||||
'@vue/shared@3.3.4': {}
|
||||
|
|
@ -8517,8 +8440,6 @@ snapshots:
|
|||
'@vue/shared@3.3.7':
|
||||
optional: true
|
||||
|
||||
'@vue/shared@3.5.13': {}
|
||||
|
||||
'@vue/test-utils@2.3.2(vue@3.3.4)':
|
||||
dependencies:
|
||||
js-beautify: 1.14.6
|
||||
|
|
@ -8546,33 +8467,26 @@ snapshots:
|
|||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/head@1.0.0(typescript@5.2.2)(vue@3.3.4)':
|
||||
'@vueuse/head@1.0.0(vue@3.3.4)':
|
||||
dependencies:
|
||||
'@unhead/schema': 0.5.1
|
||||
'@unhead/ssr': 0.5.1
|
||||
'@unhead/vue': 0.5.1(typescript@5.2.2)(vue@3.3.4)
|
||||
'@unhead/vue': 0.5.1(vue@3.3.4)
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
- '@vue/composition-api'
|
||||
|
||||
'@vueuse/metadata@10.3.0': {}
|
||||
|
||||
'@vueuse/router@10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4)':
|
||||
dependencies:
|
||||
'@vueuse/shared': 10.0.0(vue@3.3.4)
|
||||
'@vueuse/shared': 10.3.0(vue@3.3.4)
|
||||
vue-demi: 0.14.5(vue@3.3.4)
|
||||
vue-router: 4.1.6(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/shared@10.0.0(vue@3.3.4)':
|
||||
dependencies:
|
||||
vue-demi: 0.14.5(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/shared@10.3.0(vue@3.3.4)':
|
||||
dependencies:
|
||||
vue-demi: 0.14.5(vue@3.3.4)
|
||||
|
|
@ -8580,12 +8494,6 @@ snapshots:
|
|||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/shared@12.0.0(typescript@5.2.2)':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.2.2)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@zhead/schema@1.0.0-beta.13': {}
|
||||
|
||||
abab@2.0.6: {}
|
||||
|
|
@ -8780,7 +8688,7 @@ snapshots:
|
|||
|
||||
builtins@5.0.1:
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
|
||||
bundle-require@5.0.0(esbuild@0.23.1):
|
||||
dependencies:
|
||||
|
|
@ -9084,8 +8992,6 @@ snapshots:
|
|||
|
||||
csstype@3.1.2: {}
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
dash-get@1.0.2: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
|
@ -9817,9 +9723,6 @@ snapshots:
|
|||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
|
|
@ -10753,7 +10656,7 @@ snapshots:
|
|||
normalize-package-data@2.5.0:
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
resolve: 1.22.8
|
||||
resolve: 1.22.9
|
||||
semver: 5.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
|
|
@ -10972,7 +10875,11 @@ snapshots:
|
|||
|
||||
plausible-tracker@0.3.8: {}
|
||||
|
||||
playwright-core@1.32.3: {}
|
||||
playwright-core@1.63.0: {}
|
||||
|
||||
playwright@1.63.0:
|
||||
dependencies:
|
||||
playwright-core: 1.63.0
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
|
|
@ -11271,12 +11178,6 @@ snapshots:
|
|||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
resolve@1.22.8:
|
||||
dependencies:
|
||||
is-core-module: 2.13.1
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
resolve@1.22.9:
|
||||
dependencies:
|
||||
is-core-module: 2.16.0
|
||||
|
|
@ -12073,7 +11974,7 @@ snapshots:
|
|||
less: 4.1.3
|
||||
terser: 5.37.0
|
||||
|
||||
vitest@0.34.0(jsdom@22.0.0)(less@4.1.3)(terser@5.37.0):
|
||||
vitest@0.34.0(jsdom@22.0.0)(less@4.1.3)(playwright@1.63.0)(terser@5.37.0):
|
||||
dependencies:
|
||||
'@types/chai': 4.3.5
|
||||
'@types/chai-subset': 1.3.3
|
||||
|
|
@ -12101,6 +12002,7 @@ snapshots:
|
|||
why-is-node-running: 2.2.2
|
||||
optionalDependencies:
|
||||
jsdom: 22.0.0
|
||||
playwright: 1.63.0
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
|
|
@ -12125,14 +12027,14 @@ snapshots:
|
|||
|
||||
vue-eslint-parser@9.3.1(eslint@8.47.0):
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.4.0
|
||||
eslint: 8.47.0
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
lodash: 4.17.21
|
||||
semver: 7.5.4
|
||||
semver: 7.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -12180,16 +12082,6 @@ snapshots:
|
|||
'@vue/server-renderer': 3.3.4(vue@3.3.4)
|
||||
'@vue/shared': 3.3.4
|
||||
|
||||
vue@3.5.13(typescript@5.2.2):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
'@vue/runtime-dom': 3.5.13
|
||||
'@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.2.2))
|
||||
'@vue/shared': 3.5.13
|
||||
optionalDependencies:
|
||||
typescript: 5.2.2
|
||||
|
||||
vuedraggable@4.1.0(vue@3.3.4):
|
||||
dependencies:
|
||||
sortablejs: 1.14.0
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import { tool as deviceInformation } from './device-information';
|
|||
import { tool as cypher } from './encryption';
|
||||
import { tool as etaCalculator } from './eta-calculator';
|
||||
import { tool as percentageCalculator } from './percentage-calculator';
|
||||
import { tool as x509CertificateDecoder } from './x509-certificate-decoder';
|
||||
import { tool as gitMemo } from './git-memo';
|
||||
import { tool as hashText } from './hash-text';
|
||||
import { tool as hmacGenerator } from './hmac-generator';
|
||||
|
|
@ -164,7 +165,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
},
|
||||
{
|
||||
name: 'Network',
|
||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator],
|
||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator, x509CertificateDecoder],
|
||||
},
|
||||
{
|
||||
name: 'Math',
|
||||
|
|
|
|||
12
src/tools/x509-certificate-decoder/index.ts
Normal file
12
src/tools/x509-certificate-decoder/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { ShieldLock } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'X.509 certificate decoder',
|
||||
path: '/x509-certificate-decoder',
|
||||
description: 'Decode a PEM certificate (RSA, EC or Ed25519): subject, issuer, validity, public key, extensions and fingerprints',
|
||||
keywords: ['x509', 'certificate', 'pem', 'tls', 'ssl', 'decoder', 'asn1', 'expiry'],
|
||||
component: () => import('./x509-certificate-decoder.vue'),
|
||||
icon: ShieldLock,
|
||||
createdAt: new Date('2026-09-30'),
|
||||
});
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { decodeCertificatePem } from './x509-certificate-decoder.service';
|
||||
|
||||
// Regression corpus generated with OpenSSL 3.5 (`openssl req -x509`), then
|
||||
// every asserted value cross-checked against `openssl x509 -text`. These
|
||||
// pin the decoder to OpenSSL's canonical rendering of the tricky shapes:
|
||||
// IPv6 SANs, multi-byte named bits (decipherOnly), bare SKI key ids,
|
||||
// context-tagged AKI key ids, and an extension OID with a multi-byte first
|
||||
// sub-identifier (1.2.999.1 → 0x88 0x67).
|
||||
|
||||
const RSA_IPV6_CERT_PEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIDnDCCAoSgAwIBAgIUYQ5gUMphhz1LUtSnI4XJxAnGzTcwDQYJKoZIhvcNAQEL
|
||||
BQAwSTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxDTALBgNVBAoMBFRl
|
||||
c3QxGTAXBgNVBAMMEHRlc3QuZXhhbXBsZS5jb20wHhcNMjYxMDAzMDgzMjMzWhcN
|
||||
MjYxMTAyMDgzMjMzWjBJMQswCQYDVQQGEwJDTjEQMA4GA1UECAwHQmVpamluZzEN
|
||||
MAsGA1UECgwEVGVzdDEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnND40VnXTg3ZvBrfJOPh4WGmV718Cb
|
||||
URSFKnK1yDITpAc4HsJjcTntl92Xsb0ov1Nc20/L7ePz2qN4QbFqmhzZbmNhdmIF
|
||||
IXlldpFsFHRjSJ3kbwsApWtozDVi3iAYseJ2GfFZ4rUfOIoRbCkHWR6KNwpJrJT4
|
||||
IWktnE0fK7tIMRc4O+d6ksVg54tLi+r6BSZAurdWLy9Bq3zWwYUrZUvG2TBJ1HIi
|
||||
CRDC7PSyCV/o/C9eGtJr0EGDq7Exl/kfW+QYwqFMXXrfjhSWBwEZfiegUPr9/7DD
|
||||
ZMdPTmdOwrV8OmkhSdXSM5tAoRwlSako/Rirf52o02Jm7U2phlHxQP0CAwEAAaN8
|
||||
MHowDwYDVR0TAQH/BAUwAwEB/zA3BgNVHREEMDAuhxAgAQ24AAAAAAAAAAAAAAAB
|
||||
hwTAqAEBgg1hLmV4YW1wbGUuY29tgQVhQGIuYzAPBgNVHQ8BAf8EBQMDB6CAMB0G
|
||||
A1UdDgQWBBSMRZA0vuzo5oiW4uxRGmP62SZcsTANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
QerYkO13fxKoqpLitIKKdm5jDCOXfEMJbH2CwKUIhJbxsslCW32p0IXRmvudwPSO
|
||||
yFwD3fIy7vPrPxiLOvrvs1uEMEYhNKeE0yVhk2DjQxHAJpQRDCHYj4vZSPkrx6ze
|
||||
JpjEaJ9TniIlxKS4ggubyOkVM51o47rqevsmUccFgT6Umu2AMmgSP4WvUgbncYrK
|
||||
SKodoq9fZp1e2eVy7CeN7A6rpbYHAgktoYMBvtRSnZVJo+CcqcNl4xc97gtXeKQI
|
||||
vt8bIDLVFsV0CHuVIi/LzhcZvZ8khTniu4YELpzqYjfLkib8XADtWdeWGTwOPz8a
|
||||
Y1eZ1r6V6qU4MQpkyc1EdQ==
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
const ED25519_CUSTOM_OID_CERT_PEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIB1jCCAVygAwIBAgIUGd2IUBuv1j/C7zeNuXkPo96XpjQwCgYIKoZIzj0EAwIw
|
||||
MDELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlRlc3RDQTEQMA4GA1UEAwwHUm9vdCBD
|
||||
QTAeFw0yNjEwMDMxMDQ0MDhaFw0yNjExMDIxMDQ0MDhaMB4xHDAaBgNVBAMME2Vk
|
||||
MjU1MTkuZXhhbXBsZS5jb20wKjAFBgMrZXADIQAU+Pxvo65PrtplGQ2m7ro0w+Nl
|
||||
8LT+55hSlOUnchBPTaOBlDCBkTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAZBgNV
|
||||
HREEEjAQgg5lZS5leGFtcGxlLmNvbTAcBgQqh2cBBBQMEm11bHRpYnl0ZS1vaWQt
|
||||
dGVzdDAdBgNVHQ4EFgQUFP9MR1+AykhnKRueeH/chyvm8DswHwYDVR0jBBgwFoAU
|
||||
5mzqfASzZNXN4M6pjkcrzP8dfRgwCgYIKoZIzj0EAwIDaAAwZQIxALE9DbikX0km
|
||||
UVBx2tc3QM7kM2TdIDhOCQ5a0cARDcfXmpQKoPFw31fVoHvbPaDFGAIwBH8Ixj3N
|
||||
hG5ERArF3f+YnESSA8SOR0QDJxrAx/dP702H7Imct3LFSJiYB5DYkzWS
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
function rowsOf(pem: string, title: string): Map<string, string> {
|
||||
const decoded = decodeCertificatePem(pem);
|
||||
const section = decoded.sections.find(s => s.title === title);
|
||||
expect(section, `section ${title}`).toBeDefined();
|
||||
return new Map(section!.rows.map(row => [row.label, row.value]));
|
||||
}
|
||||
|
||||
describe('x509 decoder vs OpenSSL corpus (regression)', () => {
|
||||
it('renders IPv6 SANs in RFC 5952 text form and keeps IPv4 dotted', () => {
|
||||
const san = rowsOf(RSA_IPV6_CERT_PEM, 'Extensions').get('subjectAltName') ?? '';
|
||||
expect(san).toContain('IP: 2001:db8::1');
|
||||
expect(san).toContain('IP: 192.168.1.1');
|
||||
expect(san).toContain('DNS: a.example.com');
|
||||
expect(san).toContain('Email: a@b.c');
|
||||
expect(san).not.toContain('32.1.13.184');
|
||||
});
|
||||
|
||||
it('decodes named bits beyond the first byte (decipherOnly)', () => {
|
||||
const ku = rowsOf(RSA_IPV6_CERT_PEM, 'Extensions').get('keyUsage (critical)') ?? '';
|
||||
expect(ku).toContain('digitalSignature');
|
||||
expect(ku).toContain('keyEncipherment');
|
||||
expect(ku).toContain('decipherOnly');
|
||||
});
|
||||
|
||||
it('prints the bare SKI key id without the DER wrapper', () => {
|
||||
const ski = rowsOf(RSA_IPV6_CERT_PEM, 'Extensions').get('subjectKeyIdentifier') ?? '';
|
||||
expect(ski).toBe('8C:45:90:34:BE:EC:E8:E6:88:96:E2:EC:51:1A:63:FA:D9:26:5C:B1');
|
||||
});
|
||||
|
||||
it('prints the AKI key id from the context-tagged child', () => {
|
||||
const aki = rowsOf(ED25519_CUSTOM_OID_CERT_PEM, 'Extensions').get('authorityKeyIdentifier') ?? '';
|
||||
expect(aki).toBe('E6:6C:EA:7C:04:B3:64:D5:CD:E0:CE:A9:8E:47:2B:CC:FF:1D:7D:18');
|
||||
});
|
||||
|
||||
it('decodes extension OIDs with multi-byte first sub-identifiers', () => {
|
||||
const extensions = rowsOf(ED25519_CUSTOM_OID_CERT_PEM, 'Extensions');
|
||||
// 1.2.999.1 encodes its first sub-identifier as two base-128 bytes;
|
||||
// the old first-byte shortcut decoded it as 3.39.77.1.
|
||||
expect(extensions.has('1.2.999.1')).toBe(true);
|
||||
expect(extensions.has('3.39.77.1')).toBe(false);
|
||||
});
|
||||
|
||||
it('identifies the Ed25519 public key without a curve row', () => {
|
||||
const key = rowsOf(ED25519_CUSTOM_OID_CERT_PEM, 'Public key');
|
||||
expect(key.get('Algorithm')).toBe('Ed25519');
|
||||
expect(key.get('Key size')).toBe('256 bits');
|
||||
});
|
||||
|
||||
it('keeps the signature algorithm names for ECDSA issuer chains', () => {
|
||||
const cert = rowsOf(ED25519_CUSTOM_OID_CERT_PEM, 'Certificate');
|
||||
expect(cert.get('Signature algorithm')).toBe('ecdsa-with-SHA256');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { md, pki } from 'node-forge';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { decodeCertificatePem } from './x509-certificate-decoder.service';
|
||||
|
||||
const EC_CERT_PEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIBqjCCAVGgAwIBAgIUVgYjpd++yLMwyD15DLcDVQXwsscwCgYIKoZIzj0EAwIw
|
||||
KzEXMBUGA1UEAwwOZWMuZXhhbXBsZS5jb20xEDAOBgNVBAoMB0VDIFRlc3QwHhcN
|
||||
MjYwOTMwMTMzODI1WhcNMjcwOTMwMTMzODI1WjArMRcwFQYDVQQDDA5lYy5leGFt
|
||||
cGxlLmNvbTEQMA4GA1UECgwHRUMgVGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
||||
A0IABDLul3OTKmdoRTgVpAHhNCYl/cf0Kv0OtysMoyoXxazZ6l3RhMOFXuSSv7/Y
|
||||
M4ryQ0NxdIPcfp3tdvz9gofPJZWjUzBRMB0GA1UdDgQWBBTqaYh03Xxut48jrjWu
|
||||
SAwe84nD4TAfBgNVHSMEGDAWgBTqaYh03Xxut48jrjWuSAwe84nD4TAPBgNVHRMB
|
||||
Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCICWnMVnE7j/t2azIpgKwYc20+1Vo
|
||||
gpauAYH3o8+v52jhAiAW7o36o7iEd5r8yuh6CR/qJYazHp3eo274Q537m4us7Q==
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
function rsaCertPem(options: { keyUsage?: boolean } = {}): string {
|
||||
const keys = pki.rsa.generateKeyPair(1024);
|
||||
const cert = pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01AB';
|
||||
cert.validity.notBefore = new Date('2026-01-01T00:00:00Z');
|
||||
cert.validity.notAfter = new Date('2027-01-01T00:00:00Z');
|
||||
const subject = [
|
||||
{ shortName: 'CN', value: 'test.example.com' },
|
||||
{ shortName: 'O', value: 'Test Org' },
|
||||
{ shortName: 'C', value: 'DE' },
|
||||
];
|
||||
cert.setSubject(subject);
|
||||
cert.setIssuer(subject);
|
||||
const extensions: object[] = [
|
||||
{ name: 'basicConstraints', cA: true, pathLen: 1 },
|
||||
{ name: 'subjectAltName', altNames: [{ type: 2, value: 'test.example.com' }, { type: 7, ip: '10.0.0.1' }] },
|
||||
];
|
||||
if (options.keyUsage !== false) {
|
||||
extensions.push({ name: 'keyUsage', digitalSignature: true, keyEncipherment: true });
|
||||
}
|
||||
cert.setExtensions(extensions);
|
||||
cert.sign(keys.privateKey, md.sha256.create());
|
||||
return pki.certificateToPem(cert);
|
||||
}
|
||||
|
||||
function findRow(decoded: ReturnType<typeof decodeCertificatePem>, section: string, label: string) {
|
||||
return decoded.sections
|
||||
.find(s => s.title === section)
|
||||
?.rows.find(row => row.label.startsWith(label));
|
||||
}
|
||||
|
||||
describe('x509-certificate-decoder: RSA certificates', () => {
|
||||
it('decodes the certificate overview section', () => {
|
||||
const decoded = decodeCertificatePem(rsaCertPem());
|
||||
|
||||
expect(findRow(decoded, 'Certificate', 'Version')?.value).toBe('v3');
|
||||
expect(findRow(decoded, 'Certificate', 'Serial number')?.value).toBe('01:AB');
|
||||
expect(findRow(decoded, 'Certificate', 'Signature algorithm')?.value).toBe('sha256WithRSAEncryption');
|
||||
expect(findRow(decoded, 'Certificate', 'Not before')?.value).toContain('2026-01-01');
|
||||
expect(findRow(decoded, 'Certificate', 'Not after')?.value).toContain('2027-01-01');
|
||||
});
|
||||
|
||||
it('decodes subject and issuer distinguished names', () => {
|
||||
const decoded = decodeCertificatePem(rsaCertPem());
|
||||
|
||||
const subject = decoded.sections.find(s => s.title === 'Subject')?.rows;
|
||||
expect(subject?.[0]).toMatchObject({ label: expect.stringContaining('(CN)'), value: 'test.example.com' });
|
||||
expect(subject?.some(row => row.label.includes('(O)') && row.value === 'Test Org')).toBe(true);
|
||||
expect(subject?.some(row => row.label.includes('(C)') && row.value === 'DE')).toBe(true);
|
||||
|
||||
const issuer = decoded.sections.find(s => s.title === 'Issuer')?.rows;
|
||||
expect(issuer?.[0]?.value).toBe('test.example.com');
|
||||
});
|
||||
|
||||
it('decodes the RSA public key details', () => {
|
||||
const decoded = decodeCertificatePem(rsaCertPem());
|
||||
|
||||
expect(findRow(decoded, 'Public key', 'Algorithm')?.value).toBe('RSA');
|
||||
expect(findRow(decoded, 'Public key', 'Key size')?.value).toBe('1024 bits');
|
||||
expect(findRow(decoded, 'Public key', 'Exponent')?.value).toBe('65537');
|
||||
});
|
||||
|
||||
it('decodes basicConstraints, SAN and keyUsage extensions', () => {
|
||||
const decoded = decodeCertificatePem(rsaCertPem());
|
||||
|
||||
// forge's setExtensions drops pathLen from the DER; openssl fixture covers it
|
||||
expect(findRow(decoded, 'Extensions', 'basicConstraints')?.value).toBe('CA: true');
|
||||
const san = findRow(decoded, 'Extensions', 'subjectAltName')?.value ?? '';
|
||||
expect(san).toContain('DNS: test.example.com');
|
||||
expect(san).toContain('IP: 10.0.0.1');
|
||||
expect(findRow(decoded, 'Extensions', 'keyUsage')?.value).toContain('digitalSignature');
|
||||
expect(findRow(decoded, 'Extensions', 'keyUsage')?.value).toContain('keyEncipherment');
|
||||
});
|
||||
|
||||
it('computes both fingerprints as hex pairs', () => {
|
||||
const decoded = decodeCertificatePem(rsaCertPem());
|
||||
|
||||
const sha256 = findRow(decoded, 'Fingerprints', 'SHA-256')?.value ?? '';
|
||||
expect(sha256).toMatch(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/);
|
||||
const sha1 = findRow(decoded, 'Fingerprints', 'SHA-1')?.value ?? '';
|
||||
expect(sha1).toMatch(/^([0-9A-F]{2}:){19}[0-9A-F]{2}$/);
|
||||
});
|
||||
});
|
||||
|
||||
const CA_PATHLEN_PEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIBiTCCATCgAwIBAgIUHBPZrRX8pGjpag/D48ZCCL2n+rowCgYIKoZIzj0EAwIw
|
||||
GTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjYwOTMwMTQwMjQzWhcNMjgw
|
||||
OTI5MTQwMjQzWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTBZMBMGByqGSM49
|
||||
AgEGCCqGSM49AwEHA0IABDLul3OTKmdoRTgVpAHhNCYl/cf0Kv0OtysMoyoXxazZ
|
||||
6l3RhMOFXuSSv7/YM4ryQ0NxdIPcfp3tdvz9gofPJZWjVjBUMB0GA1UdDgQWBBTq
|
||||
aYh03Xxut48jrjWuSAwe84nD4TAfBgNVHSMEGDAWgBTqaYh03Xxut48jrjWuSAwe
|
||||
84nD4TASBgNVHRMBAf8ECDAGAQH/AgEBMAoGCCqGSM49BAMCA0cAMEQCIAD1pF4R
|
||||
oy3pyDv0IbID+fn3FmkbmWyb2pii6uEYyVtaAiB0lln06DKzc0bmww47Hf/ZfD/H
|
||||
hiErXsIe25bNKf1Iwg==
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
describe('x509-certificate-decoder: EC certificates', () => {
|
||||
it('decodes a prime256v1 certificate that node-forge certificateFromPem rejects', () => {
|
||||
const decoded = decodeCertificatePem(EC_CERT_PEM);
|
||||
|
||||
expect(findRow(decoded, 'Certificate', 'Signature algorithm')?.value).toBe('ecdsa-with-SHA256');
|
||||
expect(findRow(decoded, 'Public key', 'Algorithm')?.value).toBe('EC (id-ecPublicKey)');
|
||||
expect(findRow(decoded, 'Public key', 'Curve')?.value).toBe('prime256v1 (P-256)');
|
||||
expect(findRow(decoded, 'Public key', 'Key size')?.value).toBe('256 bits');
|
||||
expect(findRow(decoded, 'Extensions', 'basicConstraints')?.value).toBe('CA: true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('x509-certificate-decoder: critical extension with pathLen', () => {
|
||||
it('decodes critical marker and pathLen from an openssl-generated CA cert', () => {
|
||||
const decoded = decodeCertificatePem(CA_PATHLEN_PEM);
|
||||
const bc = decoded.sections
|
||||
.find(s2 => s2.title === 'Extensions')
|
||||
?.rows.find(row => row.label.startsWith('basicConstraints'));
|
||||
expect(bc?.label).toBe('basicConstraints (critical)');
|
||||
expect(bc?.value).toBe('CA: true, pathLen: 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('x509-certificate-decoder: error handling', () => {
|
||||
it('throws a helpful error for empty input', () => {
|
||||
expect(() => decodeCertificatePem('')).toThrow('Paste a PEM certificate');
|
||||
});
|
||||
|
||||
it('throws when no CERTIFICATE block is present', () => {
|
||||
const keyPem = '-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----';
|
||||
expect(() => decodeCertificatePem(keyPem)).toThrow('No CERTIFICATE block');
|
||||
});
|
||||
|
||||
it('throws on garbage input', () => {
|
||||
expect(() => decodeCertificatePem('not a pem at all')).toThrow();
|
||||
});
|
||||
|
||||
it('decodes the first certificate of a chain and ignores trailing blocks', () => {
|
||||
const chain = `${rsaCertPem()}\n${EC_CERT_PEM}`;
|
||||
const decoded = decodeCertificatePem(chain);
|
||||
expect(findRow(decoded, 'Public key', 'Algorithm')?.value).toBe('RSA');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
import { asn1, md as forgeMd, pem as forgePem } from 'node-forge';
|
||||
|
||||
export interface CertificateRow {
|
||||
label: string
|
||||
value: string
|
||||
hint?: string
|
||||
}
|
||||
|
||||
export interface CertificateSection {
|
||||
title: string
|
||||
rows: CertificateRow[]
|
||||
}
|
||||
|
||||
export interface DecodedCertificate {
|
||||
sections: CertificateSection[]
|
||||
}
|
||||
|
||||
/** Distinguished-name attribute OIDs (RFC 5280 / legacy PKCS#9). */
|
||||
const DN_OIDS: Record<string, string> = {
|
||||
'2.5.4.3': 'CommonName (CN)',
|
||||
'2.5.4.6': 'Country (C)',
|
||||
'2.5.4.7': 'Locality (L)',
|
||||
'2.5.4.8': 'StateOrProvince (ST)',
|
||||
'2.5.4.9': 'Street',
|
||||
'2.5.4.10': 'Organization (O)',
|
||||
'2.5.4.11': 'OrganizationalUnit (OU)',
|
||||
'2.5.4.5': 'SerialNumber',
|
||||
'2.5.4.4': 'Surname (SN)',
|
||||
'2.5.4.42': 'GivenName (GN)',
|
||||
'2.5.4.12': 'Title',
|
||||
'2.5.4.13': 'Description',
|
||||
'2.5.4.17': 'PostalCode',
|
||||
'1.2.840.113549.1.9.1': 'Email',
|
||||
'0.9.2342.19200300.100.1.25': 'DomainComponent (DC)',
|
||||
'0.9.2342.19200300.100.1.1': 'UserID (UID)',
|
||||
};
|
||||
|
||||
// Dotted strings below are ASN.1 object identifiers in dotted-arc notation,
|
||||
// not network addresses (S1313 pattern-matches some of them as IPv4).
|
||||
const SIGNATURE_ALGORITHM_OIDS: Record<string, string> = {
|
||||
'1.2.840.113549.1.1.4': 'md5WithRSAEncryption',
|
||||
'1.2.840.113549.1.1.5': 'sha1WithRSAEncryption',
|
||||
'1.2.840.113549.1.1.10': 'RSASSA-PSS',
|
||||
'1.2.840.113549.1.1.11': 'sha256WithRSAEncryption',
|
||||
'1.2.840.113549.1.1.12': 'sha384WithRSAEncryption',
|
||||
'1.2.840.113549.1.1.13': 'sha512WithRSAEncryption',
|
||||
'1.2.840.10045.4.1': 'ecdsa-with-SHA1',
|
||||
'1.2.840.10045.4.3.2': 'ecdsa-with-SHA256',
|
||||
'1.2.840.10045.4.3.3': 'ecdsa-with-SHA384',
|
||||
'1.2.840.10045.4.3.4': 'ecdsa-with-SHA512',
|
||||
'1.3.101.112': 'Ed25519',
|
||||
'1.3.101.113': 'Ed448',
|
||||
};
|
||||
|
||||
const PUBLIC_KEY_ALGORITHM_OIDS: Record<string, string> = {
|
||||
'1.2.840.113549.1.1.1': 'RSA',
|
||||
'1.2.840.10045.2.1': 'EC (id-ecPublicKey)',
|
||||
'1.3.101.110': 'X25519',
|
||||
'1.3.101.111': 'X448',
|
||||
'1.3.101.112': 'Ed25519',
|
||||
'1.3.101.113': 'Ed448',
|
||||
};
|
||||
|
||||
const CURVE_OIDS: Record<string, string> = {
|
||||
'1.2.840.10045.3.1.1': 'secp192r1 (P-192)',
|
||||
'1.2.840.10045.3.1.7': 'prime256v1 (P-256)',
|
||||
'1.3.132.0.10': 'secp256k1',
|
||||
'1.3.132.0.33': 'secp224r1 (P-224)',
|
||||
'1.3.132.0.34': 'secp384r1 (P-384)',
|
||||
'1.3.132.0.35': 'secp521r1 (P-521)',
|
||||
};
|
||||
|
||||
const EXTENSION_OIDS: Record<string, string> = {
|
||||
'2.5.29.19': 'basicConstraints',
|
||||
'2.5.29.17': 'subjectAltName',
|
||||
'2.5.29.18': 'issuerAltName',
|
||||
'2.5.29.15': 'keyUsage',
|
||||
'2.5.29.14': 'subjectKeyIdentifier',
|
||||
'2.5.29.35': 'authorityKeyIdentifier',
|
||||
'2.5.29.31': 'cRLDistributionPoints',
|
||||
'2.5.29.37': 'extKeyUsage',
|
||||
'1.3.6.1.5.5.7.1.1': 'authorityInfoAccess',
|
||||
'1.3.6.1.4.1.11129.2.4.2': 'signedCertificateTimestampList',
|
||||
};
|
||||
|
||||
const EXTENDED_KEY_USAGE_OIDS: Record<string, string> = {
|
||||
'1.3.6.1.5.5.7.3.1': 'serverAuth',
|
||||
'1.3.6.1.5.5.7.3.2': 'clientAuth',
|
||||
'1.3.6.1.5.5.7.3.3': 'codeSigning',
|
||||
'1.3.6.1.5.5.7.3.4': 'emailProtection',
|
||||
'1.3.6.1.5.5.7.3.8': 'timeStamping',
|
||||
'1.3.6.1.5.5.7.3.9': 'OCSPSigning',
|
||||
};
|
||||
|
||||
/** IPv4 = dotted decimal; IPv6 = RFC 5952 text (lowercase, longest zero run
|
||||
* compressed). Anything else falls back to dotted decimal of the bytes. */
|
||||
function decodeIpAddress(bytes: string): string {
|
||||
const octets = Array.from(bytes, ch => ch.charCodeAt(0));
|
||||
if (octets.length === 16) {
|
||||
const groups: string[] = [];
|
||||
for (let i = 0; i < 16; i += 2) {
|
||||
groups.push(((octets[i]! << 8) | octets[i + 1]!).toString(16));
|
||||
}
|
||||
// Compress the longest run of zero groups (length ≥ 2) into '::'.
|
||||
let bestStart = -1;
|
||||
let bestLength = 0;
|
||||
let currentStart = -1;
|
||||
let currentLength = 0;
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
if (groups[i] !== '0') {
|
||||
currentStart = -1;
|
||||
currentLength = 0;
|
||||
continue;
|
||||
}
|
||||
if (currentStart < 0) {
|
||||
currentStart = i;
|
||||
}
|
||||
currentLength++;
|
||||
if (currentLength > bestLength) {
|
||||
bestStart = currentStart;
|
||||
bestLength = currentLength;
|
||||
}
|
||||
}
|
||||
if (bestLength >= 2) {
|
||||
const head = groups.slice(0, bestStart).join(':');
|
||||
const tail = groups.slice(bestStart + bestLength).join(':');
|
||||
return `${head}::${tail}`;
|
||||
}
|
||||
return groups.join(':');
|
||||
}
|
||||
return octets.join('.');
|
||||
}
|
||||
|
||||
const GENERAL_NAME_TYPES: Record<number, { label: string; decode: (value: string) => string }> = {
|
||||
1: { label: 'Email', decode: value => value },
|
||||
2: { label: 'DNS', decode: value => value },
|
||||
6: { label: 'URI', decode: value => value },
|
||||
7: { label: 'IP', decode: decodeIpAddress },
|
||||
4: { label: 'DirName', decode: () => '(directory name)' },
|
||||
8: { label: 'RID', decode: () => '(registered id)' },
|
||||
};
|
||||
|
||||
/** forge types .value as string | Asn1[]; narrow to the constructed shape. */
|
||||
function asChildren(value: unknown): asn1.Asn1[] {
|
||||
return (value ?? []) as unknown as asn1.Asn1[];
|
||||
}
|
||||
|
||||
function isUniversal(node: asn1.Asn1, tag: number): boolean {
|
||||
return node.tagClass === asn1.Class.UNIVERSAL && node.type === tag;
|
||||
}
|
||||
|
||||
function primitiveValue(node: asn1.Asn1 | undefined): string {
|
||||
return typeof node?.value === 'string' ? node.value : '';
|
||||
}
|
||||
|
||||
/** forge may swap a BIT STRING's .value for a composed [asn1] array when its
|
||||
* content parses as ASN.1; the raw bytes always live in bitStringContents. */
|
||||
function bitStringValue(node: asn1.Asn1 | undefined): string {
|
||||
// bitStringContents exists at runtime but is missing from forge's type defs
|
||||
const raw = (node as unknown as { bitStringContents?: string } | undefined)?.bitStringContents
|
||||
?? primitiveValue(node);
|
||||
return raw.slice(1); // drop the leading unused-bits octet
|
||||
}
|
||||
|
||||
/** forge's fromDer keeps OBJECT IDENTIFIER values as raw DER bytes; expand
|
||||
* them to dotted notation. The first sub-identifier packs 40·arc1+arc2 and
|
||||
* may itself span multiple base-128 bytes (arc2 ≥ 40 under arc 2, e.g.
|
||||
* 2.999 → 0x88 0x67); the rest decode as plain base-128. */
|
||||
function decodeOid(node: asn1.Asn1 | undefined): string {
|
||||
const bytes = primitiveValue(node);
|
||||
if (!bytes) {
|
||||
return '';
|
||||
}
|
||||
let pending = 0;
|
||||
let first = 0;
|
||||
let index = 0;
|
||||
// Accumulate the packed first sub-identifier (continuation while high bit set).
|
||||
for (; index < bytes.length; index++) {
|
||||
const byte = bytes.charCodeAt(index);
|
||||
pending = (pending << 7) | (byte & 0x7F);
|
||||
if ((byte & 0x80) === 0) {
|
||||
first = pending;
|
||||
index++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const parts: number[] = first < 80
|
||||
? [Math.floor(first / 40), first % 40]
|
||||
: [2, first - 80];
|
||||
pending = 0;
|
||||
for (; index < bytes.length; index++) {
|
||||
const byte = bytes.charCodeAt(index);
|
||||
pending = (pending << 7) | (byte & 0x7F);
|
||||
if ((byte & 0x80) === 0) {
|
||||
parts.push(pending);
|
||||
pending = 0;
|
||||
}
|
||||
}
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function hex(bytes: string, separator = ':'): string {
|
||||
return Array.from(bytes, ch => ch.charCodeAt(0).toString(16).padStart(2, '0')).join(separator).toUpperCase();
|
||||
}
|
||||
|
||||
function toIso(date: Date): string {
|
||||
return `${date.toISOString().replace('T', ' ').replace(/\..+/, '')} UTC`;
|
||||
}
|
||||
|
||||
function parseTime(node: asn1.Asn1): Date {
|
||||
const raw = primitiveValue(node);
|
||||
// UTCTime YYMMDDHHMMSSZ or GeneralizedTime YYYYMMDDHHMMSSZ
|
||||
const utcTime = raw.length === 13;
|
||||
const year = utcTime
|
||||
? (Number(raw.slice(0, 2)) < 50 ? `20${raw.slice(0, 2)}` : `19${raw.slice(0, 2)}`)
|
||||
: raw.slice(0, 4);
|
||||
const rest = utcTime ? raw.slice(2) : raw.slice(4);
|
||||
return new Date(`${year}-${rest.slice(0, 2)}-${rest.slice(2, 4)}T${rest.slice(4, 6)}:${rest.slice(6, 8)}:${rest.slice(8, 10) || '00'}Z`);
|
||||
}
|
||||
|
||||
function walkDn(nameNode: asn1.Asn1 | undefined): CertificateRow[] {
|
||||
const rows: CertificateRow[] = [];
|
||||
const rdns = (nameNode?.value ?? []) as unknown as asn1.Asn1[];
|
||||
for (const rdnSet of rdns) {
|
||||
for (const attr of (rdnSet.value ?? []) as unknown as asn1.Asn1[]) {
|
||||
const [oidNode, valueNode] = (attr.value ?? []) as asn1.Asn1[];
|
||||
const oid = decodeOid(oidNode);
|
||||
rows.push({ label: DN_OIDS[oid] ?? oid, value: primitiveValue(valueNode) });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function describeAlgorithmIdentifier(node: asn1.Asn1 | undefined, table: Record<string, string>): { oid: string; name: string } {
|
||||
const oid = decodeOid(node?.value?.[0] as asn1.Asn1 | undefined);
|
||||
return { oid, name: table[oid] ?? oid };
|
||||
}
|
||||
|
||||
function describePublicKey(spkiNode: asn1.Asn1 | undefined): CertificateRow[] {
|
||||
const rows: CertificateRow[] = [];
|
||||
const [algorithmNode, keyNode] = (spkiNode?.value ?? []) as asn1.Asn1[];
|
||||
const { oid: algOid, name: algName } = describeAlgorithmIdentifier(algorithmNode, PUBLIC_KEY_ALGORITHM_OIDS);
|
||||
rows.push({ label: 'Algorithm', value: algName, hint: algOid });
|
||||
|
||||
const keyBytes = bitStringValue(keyNode);
|
||||
const algParams = (algorithmNode?.value ?? [])[1] as asn1.Asn1 | undefined;
|
||||
|
||||
if (algOid === '1.2.840.113549.1.1.1') {
|
||||
try {
|
||||
const rsa = asn1.fromDer(keyBytes);
|
||||
const [modulus, exponent] = asChildren(rsa.value);
|
||||
const modBytes = primitiveValue(modulus);
|
||||
const leadingZero = modBytes.charCodeAt(0) === 0 ? 1 : 0;
|
||||
rows.push({ label: 'Key size', value: `${(modBytes.length - leadingZero) * 8} bits` });
|
||||
const expBytes = primitiveValue(exponent);
|
||||
const exponentValue = Array.from(expBytes, ch => ch.charCodeAt(0)).reduce((acc, byte) => acc * 256 + byte, 0);
|
||||
rows.push({ label: 'Exponent', value: exponentValue.toString() });
|
||||
}
|
||||
catch {
|
||||
rows.push({ label: 'Key size', value: 'unparseable modulus' });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
if (algOid === '1.2.840.10045.2.1') {
|
||||
const curveOid = decodeOid(algParams);
|
||||
rows.push({ label: 'Curve', value: CURVE_OIDS[curveOid] ?? curveOid, hint: curveOid });
|
||||
rows.push({ label: 'Key size', value: `${keyBytes.charCodeAt(0) === 4 ? ((keyBytes.length - 1) / 2) * 8 : keyBytes.length * 8} bits` });
|
||||
rows.push({ label: 'Public key', value: hex(keyBytes) });
|
||||
return rows;
|
||||
}
|
||||
|
||||
rows.push({ label: 'Key size', value: `${keyBytes.length * 8} bits` });
|
||||
rows.push({ label: 'Public key', value: hex(keyBytes) });
|
||||
return rows;
|
||||
}
|
||||
|
||||
function describeExtension(extension: asn1.Asn1): CertificateRow {
|
||||
const children = (extension.value ?? []) as asn1.Asn1[];
|
||||
const [oidNode] = children;
|
||||
// critical BOOLEAN is optional and only present between OID and value
|
||||
const criticalNode = isUniversal(children[1], asn1.Type.BOOLEAN) ? children[1] : undefined;
|
||||
const valueNode = criticalNode ? children[2] : children[1];
|
||||
const oid = decodeOid(oidNode);
|
||||
const name = EXTENSION_OIDS[oid] ?? oid;
|
||||
const critical = criticalNode !== undefined && primitiveValue(criticalNode).charCodeAt(0) !== 0;
|
||||
const raw = primitiveValue(valueNode);
|
||||
const label = critical ? `${name} (critical)` : name;
|
||||
|
||||
try {
|
||||
if (oid === '2.5.29.19') {
|
||||
const bc = asn1.fromDer(raw);
|
||||
const bcChildren = asChildren(bc.value);
|
||||
const isCa = bcChildren[0] ? primitiveValue(bcChildren[0]).charCodeAt(0) !== 0 : false;
|
||||
const pathLenNode = bcChildren[1];
|
||||
const pathLen = pathLenNode ? Array.from(primitiveValue(pathLenNode), ch => ch.charCodeAt(0)).reduce((acc, b) => acc * 256 + b, 0) : undefined;
|
||||
return { label, value: `CA: ${isCa}${pathLen !== undefined ? `, pathLen: ${pathLen}` : ''}` };
|
||||
}
|
||||
if (oid === '2.5.29.17' || oid === '2.5.29.18') {
|
||||
const names = asn1.fromDer(raw);
|
||||
const parts = asChildren(names.value).map((generalName) => {
|
||||
const meta = GENERAL_NAME_TYPES[generalName.type];
|
||||
return meta ? `${meta.label}: ${meta.decode(primitiveValue(generalName))}` : `type${generalName.type}`;
|
||||
});
|
||||
return { label, value: parts.join(', ') || '(empty)' };
|
||||
}
|
||||
if (oid === '2.5.29.15') {
|
||||
const bytes = bitStringValue(asn1.fromDer(raw));
|
||||
// Named bits span up to 2 bytes (bit 9 = decipherOnly); unused-bits
|
||||
// count was already dropped with the BIT STRING prefix.
|
||||
const flags = ['digitalSignature', 'nonRepudiation', 'keyEncipherment', 'dataEncipherment', 'keyAgreement', 'keyCertSign', 'cRLSign', 'encipherOnly', 'decipherOnly'];
|
||||
const active = flags.filter((_, index) => {
|
||||
const byte = bytes.charCodeAt(index >> 3) ?? 0;
|
||||
return byte & (0b1000_0000 >> (index & 7));
|
||||
});
|
||||
return { label, value: active.join(', ') || '(none)' };
|
||||
}
|
||||
if (oid === '2.5.29.37') {
|
||||
const usages = asn1.fromDer(raw);
|
||||
const parts = asChildren(usages.value).map(node => EXTENDED_KEY_USAGE_OIDS[decodeOid(node)] ?? decodeOid(node));
|
||||
return { label, value: parts.join(', ') };
|
||||
}
|
||||
if (oid === '2.5.29.14') {
|
||||
// SKI: the extension value is a bare OCTET STRING — hex its content,
|
||||
// not the DER wrapper (openssl prints the bare key id).
|
||||
const node = asn1.fromDer(raw);
|
||||
const value = isUniversal(node, asn1.Type.OCTETSTRING) ? primitiveValue(node) : raw;
|
||||
return { label, value: hex(value) };
|
||||
}
|
||||
if (oid === '2.5.29.35') {
|
||||
// AKI: SEQUENCE; the key identifier lives in the context-tagged [0]
|
||||
// child. Fall back to the whole-der hex when the shape is unexpected.
|
||||
const first = asChildren(asn1.fromDer(raw).value)[0];
|
||||
const isKeyId = first
|
||||
&& first.tagClass === asn1.Class.CONTEXT_SPECIFIC
|
||||
&& first.type === 0;
|
||||
const value = isKeyId ? primitiveValue(first) : raw;
|
||||
return { label, value: hex(value) };
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// fall through to hex display
|
||||
}
|
||||
return { label, value: hex(raw) };
|
||||
}
|
||||
|
||||
function fingerprintHex(der: string, algorithm: 'sha1' | 'sha256'): string {
|
||||
const digest = forgeMd.algorithms[algorithm].create();
|
||||
digest.update(der);
|
||||
const hexDigest = digest.digest().toHex();
|
||||
return hexDigest.match(/.{2}/g)!.join(':').toUpperCase();
|
||||
}
|
||||
|
||||
export function decodeCertificatePem(pem: string): DecodedCertificate {
|
||||
const trimmed = pem.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error('Paste a PEM certificate to decode');
|
||||
}
|
||||
|
||||
const messages = forgePem.decode(trimmed);
|
||||
const certificateMessage = messages.find(message => message.type === 'CERTIFICATE');
|
||||
if (!certificateMessage) {
|
||||
throw new Error('No CERTIFICATE block found (this tool decodes certificates, not keys)');
|
||||
}
|
||||
|
||||
const root = asn1.fromDer(certificateMessage.body);
|
||||
if (root.type !== asn1.Type.SEQUENCE || !(Array.isArray(root.value)) || root.value.length < 3) {
|
||||
throw new Error('Input is not a valid X.509 certificate');
|
||||
}
|
||||
|
||||
const [tbsNode, outerSignatureNode] = asChildren(root.value);
|
||||
if (tbsNode.type !== asn1.Type.SEQUENCE) {
|
||||
throw new Error('Input is not a valid X.509 certificate');
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
let versionRow: CertificateRow = { label: 'Version', value: '1 (implicit)' };
|
||||
const versionNode = asChildren(tbsNode.value)[0];
|
||||
if (versionNode && versionNode.tagClass === asn1.Class.CONTEXT_SPECIFIC && versionNode.type === 0) {
|
||||
const versionValue = versionNode.value?.[0] ? primitiveValue(asChildren(versionNode.value)[0]).charCodeAt(0) : 0;
|
||||
versionRow = { label: 'Version', value: `v${(versionValue ?? 0) + 1}` };
|
||||
cursor = 1;
|
||||
}
|
||||
|
||||
const tbsChildren = asChildren(tbsNode.value);
|
||||
const serialNode = tbsChildren[cursor];
|
||||
const tbsSignatureNode = tbsChildren[cursor + 1];
|
||||
const issuerNode = tbsChildren[cursor + 2];
|
||||
const validityNode = tbsChildren[cursor + 3];
|
||||
const subjectNode = tbsChildren[cursor + 4];
|
||||
const spkiNode = tbsChildren[cursor + 5];
|
||||
const extensionsWrapper = asChildren(tbsNode.value).find(
|
||||
node => node.tagClass === asn1.Class.CONTEXT_SPECIFIC && node.type === 3,
|
||||
);
|
||||
|
||||
const tbsSignature = describeAlgorithmIdentifier(tbsSignatureNode, SIGNATURE_ALGORITHM_OIDS);
|
||||
const outerSignature = describeAlgorithmIdentifier(outerSignatureNode, SIGNATURE_ALGORITHM_OIDS);
|
||||
const serialBytes = primitiveValue(serialNode as asn1.Asn1 | undefined);
|
||||
const [notBeforeNode, notAfterNode] = asChildren(validityNode?.value);
|
||||
const notBefore = parseTime(notBeforeNode);
|
||||
const notAfter = parseTime(notAfterNode);
|
||||
|
||||
const extensionRows: CertificateRow[] = asChildren(asChildren(extensionsWrapper?.value)[0]?.value)
|
||||
.map(extension => describeExtension(extension));
|
||||
|
||||
const sections: CertificateSection[] = [
|
||||
{
|
||||
title: 'Certificate',
|
||||
rows: [
|
||||
versionRow,
|
||||
{ label: 'Serial number', value: hex(serialBytes) },
|
||||
{ label: 'Signature algorithm', value: tbsSignature.name, hint: tbsSignature.oid },
|
||||
{ label: 'Not before', value: toIso(notBefore) },
|
||||
{ label: 'Not after', value: toIso(notAfter) },
|
||||
{ label: 'Signature consistency', value: outerSignature.name === tbsSignature.name ? 'outer matches TBS' : `outer: ${outerSignature.name}` },
|
||||
],
|
||||
},
|
||||
{ title: 'Subject', rows: walkDn(subjectNode as asn1.Asn1 | undefined) },
|
||||
{ title: 'Issuer', rows: walkDn(issuerNode as asn1.Asn1 | undefined) },
|
||||
{ title: 'Public key', rows: describePublicKey(spkiNode as asn1.Asn1 | undefined) },
|
||||
{ title: 'Extensions', rows: extensionRows.length > 0 ? extensionRows : [{ label: '(none)', value: '' }] },
|
||||
{
|
||||
title: 'Fingerprints',
|
||||
rows: [
|
||||
{ label: 'SHA-256', value: fingerprintHex(certificateMessage.body, 'sha256') },
|
||||
{ label: 'SHA-1', value: fingerprintHex(certificateMessage.body, 'sha1') },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return { sections };
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<script setup lang="ts">
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { decodeCertificatePem } from './x509-certificate-decoder.service';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
|
||||
const rawCertificate = ref('');
|
||||
|
||||
const decoded = computed(() =>
|
||||
withDefaultOnError(() => decodeCertificatePem(rawCertificate.value), { sections: [] }),
|
||||
);
|
||||
|
||||
const validation = useValidation({
|
||||
source: rawCertificate,
|
||||
rules: [
|
||||
{
|
||||
validator: value => value.length === 0 || isNotThrowing(() => decodeCertificatePem(value)),
|
||||
message: 'Invalid certificate (paste a PEM CERTIFICATE block)',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const notAfterRow = computed(() =>
|
||||
decoded.value.sections
|
||||
.find(section => section.title === 'Certificate')
|
||||
?.rows.find(row => row.label === 'Not after'),
|
||||
);
|
||||
|
||||
const expiryRelative = computed(() => {
|
||||
const raw = notAfterRow.value?.value;
|
||||
if (!raw || !validation.isValid) {
|
||||
return undefined;
|
||||
}
|
||||
const date = new Date(raw.replace(' UTC', 'Z'));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
return formatDistanceToNow(date, { addSuffix: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<c-input-text
|
||||
v-model:value="rawCertificate"
|
||||
label="Certificate (PEM)"
|
||||
:validation="validation"
|
||||
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||
rows="8"
|
||||
|
||||
raw-text autofocus multiline mb-3
|
||||
/>
|
||||
|
||||
<n-alert v-if="rawCertificate.length > 0 && !validation.isValid" type="error" mb-3>
|
||||
Paste a PEM certificate (-----BEGIN CERTIFICATE-----), not a private key or CSR.
|
||||
</n-alert>
|
||||
|
||||
<template v-if="validation.isValid && rawCertificate.length > 0">
|
||||
<n-alert v-if="expiryRelative" type="info" mb-3>
|
||||
Expires {{ expiryRelative }}
|
||||
</n-alert>
|
||||
|
||||
<n-table>
|
||||
<tbody>
|
||||
<template v-for="section of decoded.sections" :key="section.title">
|
||||
<th colspan="2" class="table-header">
|
||||
{{ section.title }}
|
||||
</th>
|
||||
<tr v-for="row of section.rows" :key="section.title + row.label + row.value">
|
||||
<td class="claims" style="vertical-align: top;">
|
||||
<span font-bold>{{ row.label }}</span>
|
||||
<span v-if="row.hint" ml-2 text-xs op-60>{{ row.hint }}</span>
|
||||
</td>
|
||||
<td style="word-wrap: break-word; word-break: break-all;">
|
||||
{{ row.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.table-header {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
.claims {
|
||||
width: 260px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue