feat: initial commit
This commit is contained in:
@@ -8,6 +8,13 @@
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/redis-homebrew:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ZixuanChen.vitest-explorer"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.pnpm-store/
|
||||
test/
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es2021": true
|
||||
@@ -7,25 +7,17 @@ module.exports = {
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.{js,cjs}"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"no-loops"
|
||||
],
|
||||
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
},
|
||||
"rules": {},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
}
|
||||
"root": true
|
||||
}
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -9,15 +9,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ 18 ]
|
||||
node: [ 18, 20 ]
|
||||
|
||||
name: Node ${{ matrix.node }} sample
|
||||
name: Node ${{ matrix.node }} Release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run linting rules and tests
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm lint
|
||||
- run: pnpm test
|
||||
|
||||
19
.github/workflows/pr.yml
vendored
19
.github/workflows/pr.yml
vendored
@@ -5,20 +5,27 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
check:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ 16, 18, 20 ]
|
||||
node: [ 18, 20 ]
|
||||
|
||||
name: Node ${{ matrix.node }} sample
|
||||
name: Node ${{ matrix.node }} PR
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run linting rules and tests
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
name: install dependencies
|
||||
- run: pnpm lint
|
||||
name: linting
|
||||
- run: pnpm test
|
||||
name: testing
|
||||
- name: 'Report Coverage'
|
||||
if: always() # Also generate the report if tests are failing
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
@@ -26,12 +26,13 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: npm clean-install
|
||||
run: pnpm install
|
||||
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
|
||||
run: npm audit signatures
|
||||
run: pnpm audit signatures
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
#NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,3 +12,5 @@ uploads/
|
||||
# Code editor files
|
||||
.vscode
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
8784
package-lock.json
generated
8784
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "directus-extension-external-jwt",
|
||||
"name": "@zerosubnet/directus-extension-external-jwt",
|
||||
"description": "External JWT Directus Extension allow directus to trust tokens issued by an oauth2 or OIDC provider",
|
||||
"icon": "extension",
|
||||
"version": "1.0.0",
|
||||
@@ -10,17 +10,54 @@
|
||||
"directus-external-jwt"
|
||||
],
|
||||
"type": "module",
|
||||
"release": {
|
||||
"branches": [
|
||||
"main",
|
||||
"next",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"directus:extension": {
|
||||
"type": "hook",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "^10.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "directus-extension build",
|
||||
"dev": "directus-extension build -w --no-minify",
|
||||
"link": "directus-extension link",
|
||||
"directus": "npx directus start"
|
||||
"directus": "directus start",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"nyc": {
|
||||
"extension": [
|
||||
".ts",
|
||||
".tsx"
|
||||
],
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"report-dir": "coverage",
|
||||
"all": true,
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"check-coverage": true,
|
||||
"include": [
|
||||
"src/**/*.[tj]s?(x)"
|
||||
],
|
||||
"exclude": [
|
||||
"src/_tests_/**/*.*",
|
||||
"src/**/*.test.[tj]s?(x)"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/errors": "^0.0.2",
|
||||
@@ -28,31 +65,43 @@
|
||||
"@directus/tsconfig": "^1.0.0",
|
||||
"@directus/types": "^10.1.3",
|
||||
"@directus/utils": "^10.0.8",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/config": "^3.3.0",
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^20.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||
"@vitest/coverage-istanbul": "^0.34.1",
|
||||
"axios": "^1.4.0",
|
||||
"config": "^3.3.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-standard-with-typescript": "^37.0.0",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
|
||||
"eslint-plugin-no-loops": "^0.3.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"knex": "^2.5.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"typescript": "^5.1.6"
|
||||
"nyc": "^15.1.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6",
|
||||
"vitest": "^0.34.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"jwks-rsa": "^3.0.1",
|
||||
"openid-client": "^5.4.3",
|
||||
"sqlite3": "^5.1.6"
|
||||
"openid-client": "^5.4.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
2845
pnpm-lock.yaml
generated
2845
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
110
src/external-jwt/verify-token.test.ts
Normal file
110
src/external-jwt/verify-token.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {verify_token} from './verify-token';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import {JwksClient} from 'jwks-rsa';
|
||||
|
||||
import { AuthProvider } from './authProvider/get-auth-providers';
|
||||
import * as crypto from 'crypto';
|
||||
import { describe, expect, it} from 'vitest'
|
||||
|
||||
|
||||
const jwkJsonString1 = `{"keys":[{"p":"3O0m6YlnSufblB1oCYC8IIZzgx6ZnLhQBk1IYIuWJvP3OU5mKLtHE_l2kgMpM_Y95PUepwDeUdS4bACa156WjCXMBbnbwWRrGNt3uXZKlp0Krq8AH5GI-GYDCK6VE_JnmGimdHQRWrbuKEc6r0uaTJCAIoJOcL81GVLLKQZuvpM","kty":"RSA","q":"yhIy5xsSLda3cpG_L3X1ItYLSTz0i68KDqDSy-p9fq5GRD8-1JSzWa4nuBHDi0Xr6nv-icNFREFwEU9kAIY0DoRwytBvgxKMSt1XEptYi8_FpNqxeY-rEzLVEjGI2YHVMWSXOkFqtNCjAa0ByRzRsSEgmcZubfBiB2tC1yiK4GE","d":"OeEj21lbEYJbTiVDHqkeMaDhI-kMKUEJfT8XOJmaEL_b45Ij1l4eGvYyNiRyygq5lLM4g-tmnwVaPDCb9Nnmdwkwe9PaqQZGfq3ef-s-tlVDEZ2w4CdvmwvIyAqcYvLBF-QPmZy-RXRVYh3sfbJrjB-_RiQ2y3uWv-KQVwkqi8RAkHS-uwYeGyG4QNPU9wSMmyNPnnVFDJE-Q1xHlkIct28eiqdryOqP-NTBH_FC-IJLRk7i2cOnY5KRyR2rjpK56C4CjwhjVyHREsgFSfXfDZrnM6SPXDYerL345kbZ9qZdLUKlLHlKLe0_lC0FwC5ddexZ8nwbJxdi4AqLh1MKAQ","e":"AQAB","use":"sig","kid":"1","qi":"GtHMNkscK5xayJI4nmPH258C-JeOdFKAOQLHq1F73S9SG5XbbgJXnELym75MW0ChnnZSYkwav7FpIhTAdFYDoxLzE69xK6wJaqKgfQ1MP8GU5pNmWXlcr9taHTjp93uE-VC2xlyd5S_HCut1hK2NOhYuMTCp_S2t4WcOeP1kB3A","dp":"BqeGAobG-7ScIov4NEESaZBjLlHfop7Smj39rhrGPQogKjO4VAXAEFP1RFSgCxahqqHPeIxIJgLYQziJcXEva60_xfRhMCQMLcV-h7GOcZbtWXGf-VNy4rh_4uUPTHiCsk6EpQFR_H-CEOiEVf9a-G9pzKBMKI051jduMyAkec0","alg":"RS256","dq":"E2pmO7BtMbxUygxY-11xHVTFptbVhGpgJAGt32v7fOWP2NTe25wiE3bZWCVUzZf9T_1z-papnCJRe0hOioJalB6Dm-klHcn1cugLir0kZ-Kh0fI1ZUG5pVGYCXR6-rMv4dwRb8aDUzZMw0d1SXaca1GMiVn4mFWlhaL3vCaoGWE","n":"rmLVTsXLl0W6tbh1jFY02gu1HHWw8G6RKZ9DmQHR_LoxkIAiXx3mHu68o1ahjCnYozjt6aNKOHGoFHbqgN6FVKOJbVygxnvmEoFU_lNnHYQ0zmQE3tywqKqmIS54HAwZW6u6TMkDgm55WBI_yKAq6pJyXsIbHL6XJ79qCMYdbv9xkwtITqopY4brmStu8kj8LbNeIEwCk3073oG9fE-QF1EM37imIaLIdR06ykpGRSt0l423UUFyNJem-J2j2FHP_UdkUw-ybczVy15hUtPq0m1Vr65urRV60xW_OJ36OrbayrJdoipSFgM_P8zsLqkphm7tavx9LomX1pkbCfTVsw"}]}`
|
||||
const jwkJsonString2 = `{"keys":[{"p":"yrhAJ_9znp0fTIEuDrPzoHfg64e8iDJz0nrB4c-VZdIMlWTIwHjCOeDQVCav1t2dtP5z6Dge04T0Vo5JABEY_hIy8iGGvq8J4fh20a9OKO00o9quk-BkIJPtqFiokcnQ53Eq4YYuSFhiyKPdipgyvRMmcM9NAOazSr3L-nT608s","kty":"RSA","q":"tXmhxZqKCGdRNIWK0Pyr3_yEhEMuf0jXpMByqSVhgyDLeS2g3pSfOxxTrgPPFgGiiQ1vYHUiCyY7rQMduft4cmiFnaOYeNXGHNY9A43zQjLmlh4V8scR--e_H2IX8v2AgbQ5-P-684mTLGnG2GmX1_U02WgcDnssACpGBWV0PI0","d":"c_egspmdr6FnWepug-i35qAqM4GyQQ8OTf95ZHCyvcAJXMvJud_8soKd6MwxaSLO_pnJhd8OKbgVOBQ6tBMYlygjQDlxjdLIz9r1juABE1HujcPFJMAcE9RoTtJ_kszdMFOEoPTEG2r8wdXEgV5M6I4Da85ss8lmGwDfT6EyUK9tcnmMegUbHGGLDmgQ0ZTLwICKKZCpchDqnMxf2gRZNkbFeQSDLCL0bsBlkqTsu-dNG1lVteFuRQmRvv5wrQN5QzQG0i2MhDkQCm0M8nmg-QU62RLfITXdOJp0tf0eaX91-zF3FVE3letIBETQMNyHOeUL0Z7KK4Dmxo6dbs2cyQ","e":"AQAB","use":"sig","kid":"1","qi":"G7V1Rerlh_siUxRj0I2AY4-ZD5GwLzCuNcoxKRXnmoyQyii-bBqWHxRUY65kraErj1p2ZBYvVtaEp3-MjtBzZxF28mztgt6gpkn-anX8uS6gPawO2g1TGdle9f7oHhaJBzW3kyhxXHtUKTrzMUj9Srx54Ze6r_R5qk8zNKnuopk","dp":"jdFIULMNF7Gj68mThwWtMl2rJBrZcg6ZqG3opSirw4em9fyD1OKmPgdgtv45lX-EjNJWE-bu6drhdIwl1b4gVd41dd6ufUfHCibgOOEDNO59HQQnjZw1b_UNFfCwPQ2K797juNI-Hq52rRa2Lfc7x7pV8iWUIUVDuM3-nUCpGPE","alg":"RS256","dq":"qx0EN5GvI5tfy3k72jDVU38D6L58AlLJ2rQHqYvwtTbgBOPMMvPKbG8aTBOVWTezbS043qezsPWdAVbV2b7O5Hm_u1M9enp_skMkBsz7GWlrWRMHOQMR5weug8X3tQvo9uPcYfen7OjE1_TpJLf0EBJKgdCT2-eyJnm1ynLONiU","n":"j7SWjPUHJ8yRyvdhkmDMsTiVr7IJoGgCgLe-JjiZ193DfOyxmL2p4UxU_xrhXvAecaY1i0ddIVttX0Dy6uY1SRmc9Iu-knc8B75jjwyoXnZq5_B8WHOsPVJpC4T8N1a8rdrqqRXyefxyqnw6fRAz2JLcZa3R_y_x2Sob7qJ8ZolJkHxEfJA4BGOvAYnfoBOKQhXcp_z1YbMuiP3-UB35VjW5chokHH-LIzhZj8Q5TYmK0LIEeZxWN46DnxxJrcqriDLFIf0oVn3aafyHj7OuS6NWCdpzG8sVadOPHLpakoRiM_6-kW7U3N4vaIWZNttCFTgaVR_XbqqCGaiLRsY6zw"}]}`
|
||||
const jwksJson1 = JSON.parse(jwkJsonString1)
|
||||
const jwksJson2 = JSON.parse(jwkJsonString2)
|
||||
|
||||
const key1 = crypto.createPrivateKey({format: 'jwk', key: JSON.parse(jwkJsonString1).keys[0]})
|
||||
//const key2 = crypto.createPrivateKey({format: 'jwk', key: JSON.parse(jwkJsonString2).keys[0]})
|
||||
|
||||
const jwksClient1 = new JwksClient({
|
||||
cache: false,
|
||||
jwksUri: 'https://test.com',
|
||||
fetcher: () => { return Promise.resolve({keys:[
|
||||
{
|
||||
kty: jwksJson1.keys[0].kty,
|
||||
alg: jwksJson1.keys[0].alg,
|
||||
kid: jwksJson1.keys[0].kid,
|
||||
n: jwksJson1.keys[0].n,
|
||||
e: jwksJson1.keys[0].e,
|
||||
use: jwksJson1.keys[0].use,
|
||||
}
|
||||
]}) },
|
||||
})
|
||||
|
||||
const jwksClient2 = new JwksClient({
|
||||
cache: false,
|
||||
jwksUri: 'https://test.com',
|
||||
fetcher: () => { return Promise.resolve({keys:[
|
||||
{
|
||||
kty: jwksJson2.keys[0].kty,
|
||||
alg: jwksJson2.keys[0].alg,
|
||||
kid: jwksJson2.keys[0].kid,
|
||||
n: jwksJson2.keys[0].n,
|
||||
e: jwksJson2.keys[0].e,
|
||||
use: jwksJson2.keys[0].use,
|
||||
}
|
||||
]}) },
|
||||
})
|
||||
|
||||
describe('verify_token', () => {
|
||||
it('should reject if JWKSClient is not initialized', () => {
|
||||
return verify_token({} as AuthProvider, '').catch(err => {
|
||||
expect(err).to.equal('JWKSClient not initialized');
|
||||
})
|
||||
})
|
||||
it('should reject if token is invalid', () => {
|
||||
|
||||
const invalidToken = jwt.sign({foo: 'bar'}, 'invalidkeyforjws', {algorithm: 'HS256'});
|
||||
const provider: AuthProvider = {
|
||||
JWKSClient: jwksClient1,
|
||||
label: 'test',
|
||||
driver: 'openid',
|
||||
trusted: true,
|
||||
issuer_url: 'https://test.com',
|
||||
name: 'test',
|
||||
client_id: 'test',
|
||||
}
|
||||
return verify_token(provider, invalidToken).then((result) => {
|
||||
expect(result).to.be.undefined;
|
||||
}).catch(err => {
|
||||
expect(err).to.be.instanceOf(jwt.JsonWebTokenError);
|
||||
})
|
||||
})
|
||||
it('should accept if token is valid', async () => {
|
||||
|
||||
|
||||
const validToken = jwt.sign({foo: 'bar'}, key1, {algorithm: 'RS256', keyid: "1"});
|
||||
|
||||
const provider: AuthProvider = {
|
||||
JWKSClient: jwksClient1,
|
||||
label: 'test',
|
||||
driver: 'openid',
|
||||
trusted: true,
|
||||
issuer_url: 'https://test.com',
|
||||
name: 'test',
|
||||
client_id: 'test',
|
||||
}
|
||||
return verify_token(provider, validToken).then((result) => {
|
||||
expect(result).to.be.instanceOf(Object);
|
||||
})
|
||||
})
|
||||
it('should deny if token is signed by wrong issuer', async () => {
|
||||
|
||||
|
||||
const validToken = jwt.sign({foo: 'bar'}, key1, {algorithm: 'RS256', keyid: "1"});
|
||||
|
||||
const provider: AuthProvider = {
|
||||
JWKSClient: jwksClient2,
|
||||
label: 'test',
|
||||
driver: 'openid',
|
||||
trusted: true,
|
||||
issuer_url: 'https://test.com',
|
||||
name: 'test',
|
||||
client_id: 'test',
|
||||
}
|
||||
return verify_token(provider, validToken).catch((err) => {
|
||||
expect(err).to.be.instanceOf(jwt.JsonWebTokenError);
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
10
src/index.test.ts
Normal file
10
src/index.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {default as index} from '../src/index'
|
||||
|
||||
describe('index', () => {
|
||||
it('should export a function ', () => {
|
||||
expect(index).to.be.instanceOf(Function);
|
||||
})
|
||||
|
||||
})
|
||||
@@ -2,8 +2,11 @@ import { defineHook } from '@directus/extensions-sdk';
|
||||
import { getAccountabilityForToken } from './external-jwt/get-accountability-for-token';
|
||||
import type { Request } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { HookConfig } from '@directus/types';
|
||||
|
||||
export default defineHook(({ filter }) => {
|
||||
|
||||
|
||||
export default defineHook<HookConfig>(({ filter }) => {
|
||||
|
||||
// get all configuration
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2020"],
|
||||
"module": "es2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ES2022",
|
||||
"preserveConstEnums": true,
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"noUnusedLocals": true,
|
||||
"target": "es2022",
|
||||
"types": ["node"],
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": []
|
||||
}
|
||||
|
||||
13
vitest.config.ts
Normal file
13
vitest.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'istanbul', // or 'v8'
|
||||
reporter: ['text', 'json-summary', 'json'],
|
||||
all: true,
|
||||
include: ['src/**'],
|
||||
reportsDirectory: './coverage',
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user