diff --git a/local-tests/tests/wrapped-keys/util.ts b/local-tests/tests/wrapped-keys/util.ts index 4f3573fe33..6ca5008de9 100644 --- a/local-tests/tests/wrapped-keys/util.ts +++ b/local-tests/tests/wrapped-keys/util.ts @@ -15,6 +15,9 @@ import type { const emptyLitActionRepositoryCommon: LitActionCodeRepositoryCommon = { batchGenerateEncryptedKeys: '', + + // bespoke + tria_batchGenerateEncryptedKeys: '', }; const emptyLitActionRepository: LitActionCodeRepository = { diff --git a/packages/types/src/lib/ILitNodeClient.ts b/packages/types/src/lib/ILitNodeClient.ts index 0f8b3646b0..e040e6c61e 100644 --- a/packages/types/src/lib/ILitNodeClient.ts +++ b/packages/types/src/lib/ILitNodeClient.ts @@ -18,6 +18,8 @@ import { RejectedNodePromises, SendNodeCommand, SuccessNodePromises, + GetLitActionSessionSigs, + SessionSigsMap, } from './interfaces'; import { ILitResource, ISessionCapabilityObject } from './models'; import { SupportedJsonRequests } from './types'; @@ -227,4 +229,15 @@ export interface ILitNodeClient { generateSessionCapabilityObjectWithWildcards( litResources: ILitResource[] ): Promise; + + /** + * Retrieves session signatures specifically for Lit Actions. + * Unlike `getPkpSessionSigs`, this function requires either `litActionCode` or `litActionIpfsId`, and `jsParams` must be provided. + * + * @param params - The parameters required for retrieving the session signatures. + * @returns A promise that resolves with the session signatures. + */ + getLitActionSessionSigs( + params: GetLitActionSessionSigs + ): Promise } diff --git a/packages/wrapped-keys-lit-actions/esbuild.config.js b/packages/wrapped-keys-lit-actions/esbuild.config.js index 7b0ba54732..6050a84046 100644 --- a/packages/wrapped-keys-lit-actions/esbuild.config.js +++ b/packages/wrapped-keys-lit-actions/esbuild.config.js @@ -25,8 +25,11 @@ const wrapIIFEInStringPlugin = { result.outputFiles.forEach((outputFile) => { let content = outputFile.text; + const fileName = path.basename(outputFile.path); + // Use JSON.stringify to safely encode the content const wrappedContent = `/** + * ${fileName} * DO NOT EDIT THIS FILE. IT IS GENERATED ON BUILD. RUN \`yarn generate-lit-actions\` IN THE ROOT DIRECTORY TO UPDATE THIS FILE. * @type {string} */ @@ -58,6 +61,9 @@ module.exports = { './src/lib/ethereum/generateEncryptedEthereumPrivateKey.js', './src/lib/common/exportPrivateKey.js', './src/lib/common/batchGenerateEncryptedKeys.js', + + // bespoke + './src/lib/common/bespoke/tria_batchGenerateEncryptedKeys.js', ], bundle: true, minify: true, diff --git a/packages/wrapped-keys-lit-actions/package.json b/packages/wrapped-keys-lit-actions/package.json index e3420fbc4c..5417b0589c 100644 --- a/packages/wrapped-keys-lit-actions/package.json +++ b/packages/wrapped-keys-lit-actions/package.json @@ -24,7 +24,8 @@ "genReact": false }, "scripts": { - "generate-lit-actions": "yarn node ./esbuild.config.js" + "generate-lit-actions": "yarn node ./esbuild.config.js", + "sync": "yarn node sync-actions-to-ipfs.js" }, "version": "6.9.0", "main": "./dist/src/index.js", diff --git a/packages/wrapped-keys-lit-actions/src/index.ts b/packages/wrapped-keys-lit-actions/src/index.ts index 8ca684ec8c..e4c12c2a19 100644 --- a/packages/wrapped-keys-lit-actions/src/index.ts +++ b/packages/wrapped-keys-lit-actions/src/index.ts @@ -1,4 +1,5 @@ import * as batchGenerateEncryptedKeys from './generated/common/batchGenerateEncryptedKeys'; +import * as tria_batchGenerateEncryptedKeys from './generated/common/bespoke/tria_batchGenerateEncryptedKeys'; import * as exportPrivateKey from './generated/common/exportPrivateKey'; import * as generateEncryptedEthereumPrivateKey from './generated/ethereum/generateEncryptedEthereumPrivateKey'; import * as signMessageWithEthereumEncryptedKey from './generated/ethereum/signMessageWithEncryptedEthereumKey'; @@ -33,6 +34,9 @@ const litActionRepository: LitActionCodeRepository = { const litActionRepositoryCommon: LitActionCodeRepositoryCommon = { batchGenerateEncryptedKeys: batchGenerateEncryptedKeys.code, + + // bespoke + tria_batchGenerateEncryptedKeys: tria_batchGenerateEncryptedKeys.code, }; export { @@ -49,4 +53,7 @@ export { // Full export to bundle all lit actions litActionRepository, litActionRepositoryCommon, + + // bespoke + tria_batchGenerateEncryptedKeys, }; diff --git a/packages/wrapped-keys-lit-actions/src/lib/common/bespoke/tria_batchGenerateEncryptedKeys.js b/packages/wrapped-keys-lit-actions/src/lib/common/bespoke/tria_batchGenerateEncryptedKeys.js new file mode 100644 index 0000000000..6e5f8980f7 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/common/bespoke/tria_batchGenerateEncryptedKeys.js @@ -0,0 +1,259 @@ +/** + * Optimization of Wrapped-Key Creation Process and Onboarding + */ +const { encryptPrivateKey } = require('./../internal/encryptKey'); +const { + generateEthereumPrivateKey, +} = require('../../ethereum/internal/generatePrivateKey'); +const { + signMessageEthereumKey, +} = require('../../ethereum/internal/signMessage'); +const { + generateSolanaPrivateKey, +} = require('../../solana/internal/generatePrivateKey'); +const { signMessageSolanaKey } = require('../../solana/internal/signMessage'); + +/* TRIA:: global accessControlConditions, actions, Lit*/ +console.log('TRIA'); +async function processEthereumAction(action) { + const { network, generateKeyParams } = action; + const messageToSign = action.signMessageParams?.messageToSign; + + const ethereumKey = generateEthereumPrivateKey(); + + const [generatedPrivateKey, messageSignature] = await Promise.all([ + encryptPrivateKey({ + accessControlConditions, + publicKey: ethereumKey.publicKey, + privateKey: ethereumKey.privateKey, + }), + messageToSign + ? signMessageEthereumKey({ + messageToSign: messageToSign, + privateKey: ethereumKey.privateKey, + }) + : Promise.resolve(), + ]); + + return { + network, + generateEncryptedPrivateKey: { + ...generatedPrivateKey, + memo: generateKeyParams.memo, + }, + ...(messageSignature + ? { signMessage: { signature: messageSignature } } + : {}), + }; +} + +async function processSolanaAction(action) { + const { network, generateKeyParams } = action; + + const messageToSign = action.signMessageParams?.messageToSign; + + const solanaKey = generateSolanaPrivateKey(); + + const [generatedPrivateKey, messageSignature] = await Promise.all([ + encryptPrivateKey({ + accessControlConditions, + publicKey: solanaKey.publicKey, + privateKey: solanaKey.privateKey, + }), + messageToSign + ? signMessageSolanaKey({ + messageToSign: messageToSign, + privateKey: solanaKey.privateKey, + }) + : Promise.resolve(), + ]); + + return { + network, + generateEncryptedPrivateKey: { + ...generatedPrivateKey, + memo: generateKeyParams.memo, + }, + ...(messageSignature + ? { signMessage: { signature: messageSignature } } + : {}), + }; +} + +async function processActions(actions) { + return Promise.all( + actions.map(async (action, ndx) => { + const { network } = action; + + if (network === 'evm') { + return await processEthereumAction(action, ndx); + } else if (network === 'solana') { + return await processSolanaAction(action, ndx); + } else { + // Just in case :tm: + throw new Error(`Invalid network for action[${ndx}]: ${network}`); + } + }) + ); +} + +// (async () => { +// try { +// validateParams(actions); + +// const batchGeneratePrivateKeysActionResult = await processActions(actions); + +// Lit.Actions.setResponse({ +// response: JSON.stringify(batchGeneratePrivateKeysActionResult), +// }); + +// // 1. Generate both EVM and solana private keys +// // 2. Run appropriate signMessage for each key _and_ encrypt the keys for persistence to wrapped-keys backend +// // 3. Return results for both signMessage ops and both encrypted key payloads for persistence +// } catch (err) { +// Lit.Actions.setResponse({ response: `Error: ${err.message}` }); +// } +// })(); + +/** + * - jsParams: Expected data type: Object (e.g., "{ authMethod: { accessToken: '...', authMethodType: '...' }, publicKey: '...', actions: [...] }") + * + * This parameter is an object containing the following properties: + * - authMethod + * - publicKey + * - actions: Array of action objects, each containing network and key generation params. + * + */ +function validateJsParams(jsParams) { + if (!jsParams.authMethod) { + throw new Error('Missing required field: authMethod'); + } + if (!jsParams.publicKey) { + throw new Error('Missing required field: publicKey'); + } + + const { accessToken, authMethodType } = jsParams.authMethod; + + if (!accessToken) { + throw new Error('Missing required field: authMethod.accessToken'); + } + if (!authMethodType) { + throw new Error('Missing required field: authMethod.authMethodType'); + } + + if (!jsParams.actions) { + throw new Error('Missing required field: actions'); + } + + if (!jsParams.actions.length) { + throw new Error('No actions provided (empty array?)'); + } + + jsParams.actions.forEach((action, ndx) => { + if (!['evm', 'solana'].includes(action.network)) { + throw new Error( + `Invalid field: actions[${ndx}].network: ${action.network}` + ); + } + + if (!action.generateKeyParams) { + throw new Error( + `Missing required field: actions[${ndx}].generateKeyParams` + ); + } + + if (!action.generateKeyParams?.memo) { + throw new Error( + `Missing required field: actions[${ndx}].generateKeyParams.memo` + ); + } + + if (action.signMessageParams && !action.signMessageParams?.messageToSign) { + throw new Error( + `Missing required field: actions[${ndx}].signMessageParams.messageToSign` + ); + } + }); +} + +const go = async () => { + // Lit Action:: Prepare jsParams + const jsParams = { + authMethod: { + accessToken: authMethod.accessToken, + authMethodType: authMethod.authMethodType, + }, + publicKey: publicKey, + actions: actions, + }; + + validateJsParams(jsParams); + + // Authentication + const url = 'https://api.development.tria.so/api/v1/user/info'; + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${jsParams.authMethod.accessToken}`, + }, + }); + const data = await response.json(); + console.log('data', data); + + if (!data.success) { + Lit.Actions.setResponse({ + response: JSON.stringify({ + success: false, + message: 'Authentication Failed', + }), + }); + return; + } + + // Authorization:: Prepare params + // -- 1. get the authMethodId from unique identify from the response + const authMethodId = `${ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(data.userInfo.uuid) + )}`; + console.log('Computed AuthMethodId', authMethodId); + + // -- 2. get the PKP token id + const tokenId = Lit.Actions.pubkeyToTokenId({ + publicKey: jsParams.publicKey, + }); + console.log('tokenId', tokenId); + + // -- 3. get the permitted auth methods of the PKP token id + const permittedAuthMethods = await Lit.Actions.getPermittedAuthMethods({ + tokenId, + }); + console.log('permittedAuthMethods', permittedAuthMethods); + + // -- 4. only get where authMethod that's equal to the authMethod Id + const permittedAuthMethod = permittedAuthMethods.find( + (method) => method.id === authMethodId + ); + console.log('permittedAuthMethod', permittedAuthMethod); + + // Authorization:: Failed Authentication and Authorization + if ( + !permittedAuthMethod || + permittedAuthMethod.auth_method_type !== jsParams.authMethod.authMethodType + ) { + Lit.Actions.setResponse({ + response: JSON.stringify({ + success: false, + message: 'Authorization Failed', + }), + }); + return; + } + + // Authorization:: Successful Authentication and Authorization + //LitActions.setResponse({ success: true, response: "true" }); + LitActions.setResponse({ + response: "(true, 'Anything your want to use in executeJs')", + }); +}; + +go(); diff --git a/packages/wrapped-keys/src/index.ts b/packages/wrapped-keys/src/index.ts index ea0b95d524..7286215edc 100644 --- a/packages/wrapped-keys/src/index.ts +++ b/packages/wrapped-keys/src/index.ts @@ -8,6 +8,9 @@ import { storeEncryptedKey, listEncryptedKeyMetadata, batchGeneratePrivateKeys, + + // bespoke + triaBatchGeneratePrivateKeys, } from './lib/api'; import { CHAIN_ETHEREUM, @@ -71,6 +74,9 @@ export const api = { signTransactionWithEncryptedKey, storeEncryptedKey, batchGeneratePrivateKeys, + + // bespoke + triaBatchGeneratePrivateKeys, }; export const config = { diff --git a/packages/wrapped-keys/src/lib/api/bespoke/tria-batch-generate-private-keys.ts b/packages/wrapped-keys/src/lib/api/bespoke/tria-batch-generate-private-keys.ts new file mode 100644 index 0000000000..93cb98b1da --- /dev/null +++ b/packages/wrapped-keys/src/lib/api/bespoke/tria-batch-generate-private-keys.ts @@ -0,0 +1,87 @@ +import { getKeyTypeFromNetwork } from '../utils'; +import { batchGenerateKeysWithLitAction } from '../../lit-actions-client'; +import { getLitActionCodeOrCidCommon } from '../../lit-actions-client/utils'; +import { storePrivateKey } from '../../service-client'; +import { + BatchGeneratePrivateKeysActionResult, + BatchGeneratePrivateKeysParams, + BatchGeneratePrivateKeysResult, +} from '../../types'; +import { + getFirstSessionSig, + getPkpAccessControlCondition, + getPkpAddressFromSessionSig, +} from '../../utils'; +import { triaBatchGenerateKeysWithLitActionSessionSigs } from '../../lit-actions-client/bespoke/tria-batch-generate-keys'; +import { AccessControlConditions } from '@lit-protocol/types'; + +const CACHE_KEY = 'tria_batchGenerateEncryptedKeys'; + +type TriaBatchGeneratePrivateKeysParams = + Omit & { + authMethod: { + accessToken: string | `eyJ${string}`; + authMethodType: string | `0x${string}`; + }, + publicKey: string; + // accessControlConditions: AccessControlConditions + }; + +/** + * @link ../batch-generate-private-keys.ts + */ +export async function triaBatchGeneratePrivateKeys( + params: TriaBatchGeneratePrivateKeysParams +): Promise { + const { + // pkpSessionSigs, + litNodeClient } = params; + + // const sessionSig = getFirstSessionSig(pkpSessionSigs); + // const pkpAddress = getPkpAddressFromSessionSig(sessionSig); + + // const allowPkpAddressToDecrypt = getPkpAccessControlCondition(pkpAddress); + + const { litActionCode, litActionIpfsCid } = + getLitActionCodeOrCidCommon(CACHE_KEY); + + const actionResults = await triaBatchGenerateKeysWithLitActionSessionSigs({ + ...params, + litActionIpfsCid: litActionCode ? undefined : litActionIpfsCid, + litActionCode: litActionCode ? litActionCode : undefined, + // accessControlConditions: undefined, + // pkpSessionSigs, + }); + + // const results = await Promise.all( + // actionResults.map( + // async (result): Promise => { + // const { generateEncryptedPrivateKey, network } = result; + + // const signature = result.signMessage?.signature; + + // const { id } = await storePrivateKey({ + // sessionSig, + // storedKeyMetadata: { + // ...generateEncryptedPrivateKey, + // keyType: getKeyTypeFromNetwork(network), + // pkpAddress, + // }, + // litNetwork: litNodeClient.config.litNetwork, + // }); + + // return { + // ...(signature ? { signMessage: { signature } } : {}), + // generateEncryptedPrivateKey: { + // memo: generateEncryptedPrivateKey.memo, + // id, + // generatedPublicKey: generateEncryptedPrivateKey.publicKey, + // pkpAddress, + // }, + // }; + // } + // ) + // ); + + return { pkpAddress: null as any, results: null as any }; +} diff --git a/packages/wrapped-keys/src/lib/api/index.ts b/packages/wrapped-keys/src/lib/api/index.ts index d13292a017..9ca3b1ae9a 100644 --- a/packages/wrapped-keys/src/lib/api/index.ts +++ b/packages/wrapped-keys/src/lib/api/index.ts @@ -8,6 +8,9 @@ import { signMessageWithEncryptedKey } from './sign-message-with-encrypted-key'; import { signTransactionWithEncryptedKey } from './sign-transaction-with-encrypted-key'; import { storeEncryptedKey } from './store-encrypted-key'; +// bespoke +import { triaBatchGeneratePrivateKeys } from './bespoke/tria-batch-generate-private-keys'; + export { listEncryptedKeyMetadata, generatePrivateKey, @@ -18,4 +21,7 @@ export { storeEncryptedKey, getEncryptedKey, batchGeneratePrivateKeys, + + // bespoke + triaBatchGeneratePrivateKeys, }; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/bespoke/tria-batch-generate-keys.ts b/packages/wrapped-keys/src/lib/lit-actions-client/bespoke/tria-batch-generate-keys.ts new file mode 100644 index 0000000000..89bda2f158 --- /dev/null +++ b/packages/wrapped-keys/src/lib/lit-actions-client/bespoke/tria-batch-generate-keys.ts @@ -0,0 +1,97 @@ +import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; +import { AccessControlConditions } from '@lit-protocol/types'; + +import { postLitActionValidation } from '../utils'; +import { BatchGeneratePrivateKeysParams, Network } from '../../types'; + +import { LitActionResource, LitPKPResource } from '@lit-protocol/auth-helpers'; +import { LitAbility, LitResourceAbilityRequest } from '@lit-protocol/types'; + + +interface BatchGeneratePrivateKeysWithLitActionParams + extends BatchGeneratePrivateKeysParams { + accessControlConditions: AccessControlConditions; + litActionIpfsCid?: string; + litActionCode?: string; +} + +interface GeneratePrivateKeyLitActionResult { + ciphertext: string; + dataToEncryptHash: string; + publicKey: string; + memo: string; +} + +interface BatchGeneratePrivateKeysWithLitActionResult { + network: Network; + signMessage?: { signature: string }; + generateEncryptedPrivateKey: GeneratePrivateKeyLitActionResult; +} + +export async function triaBatchGenerateKeysWithLitActionSessionSigs( + args: Omit & { + authMethod: { + accessToken: string | `eyJ${string}`; + authMethodType: string | `0x${string}`; + }, + publicKey: string; + } +): Promise { + + // print all the params out + // console.log("args:", args); + // process.exit(); + + const sessionSigs = await args.litNodeClient.getLitActionSessionSigs({ + pkpPublicKey: args.publicKey, + resourceAbilityRequests: [ + { + resource: new LitPKPResource('*'), + ability: LitAbility.PKPSigning, + }, + { + resource: new LitActionResource('*'), + ability: LitAbility.LitActionExecution, + }, + ], + litActionCode: args.litActionCode ? + Buffer.from(args.litActionCode).toString('base64') : + undefined, + + // @ts-ignore - this is a test + litActionIpfsCid: args.litActionIpfsCid ? + undefined : + args.litActionIpfsCid, + + jsParams: { + publicKey: args.publicKey, + sigName: 'tria-batch-generate-keys-combined-sig', + actions: args.actions, + authMethod: { + accessToken: args.authMethod.accessToken, + authMethodType: args.authMethod.authMethodType, + } + } + }); + + console.log("sessionSigs:", sessionSigs); + process.exit(); + + // const result = await litNodeClient.executeJs({ + // useSingleNode: true, + // sessionSigs: pkpSessionSigs, + // ipfsId: litActionIpfsCid, + // code: litActionCode, + // jsParams: { + // actions, + // accessControlConditions, + // }, + // ipfsOptions: { + // overwriteCode: + // GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK[litNodeClient.config.litNetwork], + // }, + // }); + + // const response = postLitActionValidation(result); + // return JSON.parse(response); +} diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts index 54363e47a2..2323d99a5c 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts @@ -53,11 +53,9 @@ function assertIsLitActionRepositoryEntry( ): asserts entry is LitActionCodeRepositoryEntry { if ( typeof entry !== 'object' || - !entry || - // @ts-expect-error assert function - ('evm' in entry && typeof entry.evm !== 'string') || - // @ts-expect-error assert function - ('solana' in entry && typeof entry.solana !== 'string') || + entry === null || // Ensure entry is not null + ('evm' in entry && typeof (entry as any).evm !== 'string') || + ('solana' in entry && typeof (entry as any).solana !== 'string') || Object.keys(entry).some((key) => !['evm', 'solana'].includes(key)) ) { throw new Error( @@ -89,6 +87,9 @@ function setLitActionsCode(repository: LitActionCodeRepositoryInput) { */ const litActionCodeRepositoryCommon: LitActionCodeRepositoryCommon = { batchGenerateEncryptedKeys: '', + + // bespoke + tria_batchGenerateEncryptedKeys: '', }; function assertIsLitActionKeyCommon( diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts b/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts index 83360f84fc..9be7ddc7af 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts @@ -21,6 +21,7 @@ const LIT_ACTION_CID_REPOSITORY: LitCidRepository = Object.freeze({ const LIT_ACTION_CID_REPOSITORY_COMMON: LitCidRepositoryCommon = Object.freeze({ batchGenerateEncryptedKeys: 'QmR8Zs7ctSEctxBrSnAYhMXFXCC1ub8K1xvMn5Js3NCSAA', + tria_batchGenerateEncryptedKeys: 'QmR8Zs7', }); export { LIT_ACTION_CID_REPOSITORY, LIT_ACTION_CID_REPOSITORY_COMMON }; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/types.ts b/packages/wrapped-keys/src/lib/lit-actions-client/types.ts index bda28ecd57..ce87c80680 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/types.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/types.ts @@ -6,7 +6,17 @@ export type LitActionType = | 'generateEncryptedKey' | 'exportPrivateKey'; -export type LitActionTypeCommon = 'batchGenerateEncryptedKeys'; +export type LitActionTypeCommon = + | 'batchGenerateEncryptedKeys' + | BespokeLitActionTypes; + +export type BespokeLitActionTypes = Tria_LitActionTypeCommon; +// | Foo_LitActionTypeCommon +// | Bar_LitActionTypeCommon + +export type Tria_LitActionTypeCommon = 'tria_batchGenerateEncryptedKeys'; +// export type Foo_LitActionTypeCommon = 'FooBatchGenerateEncryptedKeys'; +// export type Bar_LitActionTypeCommon = 'BarBatchGenerateEncryptedKeys'; export type LitCidRepositoryEntry = Readonly>; diff --git a/tsconfig.json b/tsconfig.json index d79d1c4238..e8f4e19b50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,15 +10,24 @@ "importHelpers": true, "target": "ES2020", "module": "ES2020", - "lib": ["ES2020", "dom", "ES2021.String"], + "lib": [ + "ES2020", + "dom", + "ES2021.String" + ], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "paths": { - "@lit-protocol/*": ["packages/*/src"] + "@lit-protocol/*": [ + "packages/*/src" + ] } }, - "exclude": ["node_modules", "tmp"] -} + "exclude": [ + "node_modules", + "tmp" + ] +} \ No newline at end of file