diff --git a/jest.config.base.ts b/jest.config.base.ts index 3b28635e2..d9c83c79f 100644 --- a/jest.config.base.ts +++ b/jest.config.base.ts @@ -90,7 +90,15 @@ const config: JestConfigWithTsJest = { ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, + // moduleNameMapper: { + // '^@metamask/sdk$': '/packages/sdk/src/index.ts', + // '^@metamask/sdk-communication-layer$': '/packages/sdk-communication-layer/src/index.ts', + // '^@metamask/analytics-client$': '/packages/analytics-client/src/index.ts', + // '^@metamask/sdk-types$': '/packages/sdk-types/src/index.ts', + // '^@metamask/sdk-install-modal-web$': '/packages/sdk-install-modal-web/src/index.ts', + // '^@metamask/sdk-react$': '/packages/sdk-react/src/index.ts', + // // Add react-native if needed + // }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], @@ -176,7 +184,7 @@ const config: JestConfigWithTsJest = { }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: ['/node_modules/', '\\.pnp\\.[^\\/]+$'], + transformIgnorePatterns: ['/node_modules/', '\\\\.pnp\\\\.[^\\\\/]+$'], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/package.json b/package.json index 79aa83808..e6a6bd3c1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "packages/sdk-react-native", "packages/sdk-react-ui", "packages/sdk-ui", + "packages/sdk-types", "packages/sdk-lab", "packages/devsocket", "packages/devreact", diff --git a/packages/analytics-client/.eslintrc.js b/packages/analytics-client/.eslintrc.js new file mode 100644 index 000000000..60694e349 --- /dev/null +++ b/packages/analytics-client/.eslintrc.js @@ -0,0 +1,34 @@ +const path = require('path'); + +/** + * @type {import('eslint').Linter.Config} + */ +module.exports = { + extends: ['@metamask/eslint-config-typescript', '../../.eslintrc.js'], + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + project: [path.resolve(__dirname, 'tsconfig.eslint.json')], + }, + + ignorePatterns: [ + '.prettierrc.js', + '**/.eslintrc.js', + '**/jest.config.ts', + '**/dist*/', + ], + + overrides: [ + { + files: ['**/*.ts'], + rules: { + // Add any specific rule overrides for analytics-client here if needed + // Example: + // '@typescript-eslint/consistent-type-definitions': [ + // 'error', + // 'interface', + // ], + }, + }, + ], +}; \ No newline at end of file diff --git a/packages/analytics-client/package.json b/packages/analytics-client/package.json index 623a2edfd..7e325bd15 100644 --- a/packages/analytics-client/package.json +++ b/packages/analytics-client/package.json @@ -1,27 +1,63 @@ { - "name": "@metamask/sdk-analytics-client", - "packageManager": "yarn@3.5.1", + "name": "@metamask/analytics-client", + "version": "0.1.0", + "description": "Client library for sending analytics events for MetaMask SDK", + "homepage": "https://github.com/MetaMask/metamask-sdk#readme", + "bugs": { + "url": "https://github.com/MetaMask/metamask-sdk/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/metamask-sdk.git", + "directory": "packages/analytics-client" + }, + "license": "MIT", + "author": "MetaMask", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "/dist" + ], "scripts": { - "build": "echo 'Na'", - "build:dev": "echo 'Na'", - "dev": "echo 'Na'", - "build:post-tsc": "echo 'Na'", - "build:pre-tsc": "echo 'Na'", - "size": "echo 'Na'", - "clean": "echo 'Na'", - "lint": "echo 'Na'", - "lint:changelog": "echo 'Na'", - "lint:eslint": "echo 'Na'", - "lint:fix": "echo 'Na'", - "lint:misc": "echo 'Na'", - "publish:preview": "echo 'Na'", - "prepack": "echo 'Na'", - "reset": "echo 'Na'", - "test": "echo 'Na'", - "test:e2e": "echo 'Na'", - "test:coverage": "echo 'Na'", - "test:ci": "echo 'Na'", - "test:dev": "echo 'Na'", - "watch": "echo 'Na'" - } + "build": "yarn build:clean && tsc --project tsconfig.json", + "build:types": "tsc --project tsconfig.json --emitDeclarationOnly --outDir dist/types", + "build:clean": "rimraf ./dist && rimraf tsconfig.tsbuildinfo", + "clean": "rimraf ./dist && rimraf tsconfig.tsbuildinfo", + "test": "echo N/A", + "test:unit": "echo N/A", + "test:ci": "echo N/A", + "allow-scripts": "echo N/a", + "lint": "yarn lint:eslint && yarn lint:misc --check", + "lint:eslint": "eslint . --cache --ext js,ts", + "lint:changelog": "echo na", + "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", + "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path ../../.gitignore", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@metamask/sdk-types": "workspace:^", + "cross-fetch": "^4.0.0" + }, + "devDependencies": { + "@lavamoat/allow-scripts": "^2.3.1", + "@types/analytics-node": "^3.1.13", + "@types/body-parser": "^1.19.4", + "@types/cors": "^2.8.15", + "@types/express": "^4.17.20", + "@types/node": "^20.4.1", + "@typescript-eslint/eslint-plugin": "^4.20.0", + "@typescript-eslint/parser": "^4.20.0", + "eslint": "^7.30.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^3.4.0", + "prettier": "^2.8.8", + "rimraf": "^6.0.1", + "ts-node": "^10.9.1", + "typescript": "^4.3.5" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "packageManager": "yarn@3.5.1" } diff --git a/packages/sdk-communication-layer/src/Analytics.ts b/packages/analytics-client/src/Analytics.ts similarity index 73% rename from packages/sdk-communication-layer/src/Analytics.ts rename to packages/analytics-client/src/Analytics.ts index 80cbc8480..e8a3cb749 100644 --- a/packages/sdk-communication-layer/src/Analytics.ts +++ b/packages/analytics-client/src/Analytics.ts @@ -1,19 +1,17 @@ import crossFetch from 'cross-fetch'; -import { CommunicationLayerPreference } from './types/CommunicationLayerPreference'; -import { OriginatorInfo } from './types/OriginatorInfo'; -import { TrackingEvents } from './types/TrackingEvent'; -import { logger } from './utils/logger'; +import { type OriginatorInfo, TrackingEvent } from '@metamask/sdk-types'; // Use types package -export interface AnalyticsProps { +// Changed to type as per lint rule +export type AnalyticsProps = { id: string; - event: TrackingEvents; - originatorInfo?: OriginatorInfo; - commLayer?: CommunicationLayerPreference; + event: TrackingEvent; // Reverted back to enum type + originatorInfo?: OriginatorInfo; // Now uses local type + // commLayer?: CommunicationLayerPreference; // Removed prop sdkVersion?: string; commLayerVersion?: string; walletVersion?: string; params?: Record; -} +}; // Buffer for storing events let analyticsBuffer: AnalyticsProps[] = []; @@ -63,10 +61,6 @@ async function sendBufferedEvents(parameters: AnalyticsProps) { const body = JSON.stringify(flatParams); - logger.RemoteCommunication( - `[sendBufferedEvents] Sending ${analyticsBuffer.length} analytics events to ${serverUrl}`, - ); - try { const response = await crossFetch(serverUrl, { method: 'POST', @@ -77,14 +71,22 @@ async function sendBufferedEvents(parameters: AnalyticsProps) { body, }); - const text = await response.text(); - logger.RemoteCommunication(`[sendBufferedEvents] Response: ${text}`); + // Check if the request was successful before clearing the buffer + if (!response.ok) { + // Log a warning if the request failed + console.warn( + `[Analytics] Failed to send analytics event: ${response.status} ${response.statusText}`, + ); + // Optionally, handle the error differently, e.g., retry or log more details + // For now, we proceed to clear the buffer even on failure to prevent buildup + } // Clear the processed buffer --- operation is atomic and no race condition can happen since we use a separate buffer // eslint-disable-next-line require-atomic-updates analyticsBuffer.length = 0; } catch (error) { - console.warn(`Error sending analytics`, error); + // console.warn is kept as a basic logging mechanism for now + console.warn(`[Analytics] Error sending analytics`, error); } } diff --git a/packages/analytics-client/src/index.ts b/packages/analytics-client/src/index.ts new file mode 100644 index 000000000..7622d8648 --- /dev/null +++ b/packages/analytics-client/src/index.ts @@ -0,0 +1,9 @@ +export { SendAnalytics } from './Analytics'; +export type { AnalyticsProps } from './Analytics'; + +// Removed re-exports from communication-layer +// export type { +// TrackingEvents, +// OriginatorInfo, +// CommunicationLayerPreference, +// } from '@metamask/sdk-communication-layer'; diff --git a/packages/analytics-client/tsconfig.eslint.json b/packages/analytics-client/tsconfig.eslint.json new file mode 100644 index 000000000..c463f670b --- /dev/null +++ b/packages/analytics-client/tsconfig.eslint.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "*.js" + // Include test files if applicable and not covered by the main tsconfig + // "test/**/*.ts" + ], + "compilerOptions": { + "noEmit": true // Important: ESLint only needs parsing, not compilation output + } +} diff --git a/packages/analytics-client/tsconfig.json b/packages/analytics-client/tsconfig.json new file mode 100644 index 000000000..001d4ade0 --- /dev/null +++ b/packages/analytics-client/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true + // Override or add any specific compiler options for this package if needed + // e.g., "lib": ["es2020", "dom"] + }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" // Exclude tests from main build if separate test command used + ], + "references": [ + // Add references to other workspace packages this package depends on + { "path": "../sdk-types" } // Added reference to sdk-types + ] +} diff --git a/packages/analytics-server/.eslintrc.js b/packages/analytics-server/.eslintrc.js index cb49074d4..8f36ebe28 100644 --- a/packages/analytics-server/.eslintrc.js +++ b/packages/analytics-server/.eslintrc.js @@ -1,19 +1,30 @@ +const path = require('path'); + +/** + * @type {import('eslint').Linter.Config} + */ module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', + '@metamask/eslint-config-nodejs', // Use nodejs config for server + '@metamask/eslint-config-typescript', + '../../.eslintrc.js' // Extend the root config ], - env: { - node: true, - es6: true, + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + project: [path.resolve(__dirname, 'tsconfig.eslint.json')], // Point to eslint tsconfig }, + + ignorePatterns: [ + '.prettierrc.js', + '**/.eslintrc.js', + '**/jest.config.ts', + '**/dist*/', + ], + + // Remove env and basic rules as they are likely handled by extended configs + // Keep specific overrides if necessary rules: { - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - }, + 'node/no-process-env': 'off', + } }; \ No newline at end of file diff --git a/packages/analytics-server/src/index.ts b/packages/analytics-server/src/index.ts index d7e80c39e..dfa94b225 100644 --- a/packages/analytics-server/src/index.ts +++ b/packages/analytics-server/src/index.ts @@ -1,12 +1,12 @@ +/* eslint-disable import/first */ +import crypto from 'crypto'; import dotenv from 'dotenv'; - // Dotenv must be loaded before importing local files dotenv.config(); import Analytics from 'analytics-node'; import bodyParser from 'body-parser'; import cors from 'cors'; -import crypto from 'crypto'; import express from 'express'; import { rateLimit } from 'express-rate-limit'; import helmet from 'helmet'; @@ -27,143 +27,140 @@ app.disable('x-powered-by'); // Rate limiting configuration const limiter = rateLimit({ - windowMs: 60 * 1000, // 1 minute - // This high limit is effectively unused as rate limiting is primarily handled - // at the infrastructure level (e.g., Cloudflare). It was retained from a previous configuration. - max: 20, // limit each IP to max requests per windowMs - legacyHeaders: false, + windowMs: 60 * 1000, // 1 minute + // This high limit is effectively unused as rate limiting is primarily handled + // at the infrastructure level (e.g., Cloudflare). It was retained from a previous configuration. + max: 20, // limit each IP to max requests per windowMs + legacyHeaders: false, }); app.use(limiter); const analytics = new Analytics( - IS_DEV - ? process.env.SEGMENT_API_KEY_DEBUG || '' - : process.env.SEGMENT_API_KEY_PRODUCTION || '', - { - flushInterval: IS_DEV ? 1000 : 10000, - errorHandler: (err: Error) => { - logger.error(`ERROR> Analytics-node flush failed: ${err}`); - }, + IS_DEV ? process.env.SEGMENT_API_KEY_DEBUG || '' : process.env.SEGMENT_API_KEY_PRODUCTION || '', + { + flushInterval: IS_DEV ? 1000 : 10000, + errorHandler: (err: Error) => { + logger.error(`ERROR> Analytics-node flush failed: ${err}`); }, + }, ); app.get('/', (req, res) => { - if (IS_DEV) { - logger.info(`health check from`, { - 'x-forwarded-for': req.headers['x-forwarded-for'], - 'cf-connecting-ip': req.headers['cf-connecting-ip'], - }); - } - res.json({ success: true }); + if (IS_DEV) { + logger.info(`health check from`, { + 'x-forwarded-for': req.headers['x-forwarded-for'], + 'cf-connecting-ip': req.headers['cf-connecting-ip'], + }); + } + res.json({ success: true }); }); app.post('/evt', async (req, res) => { - try { - const { body } = req; - - if (!body.event) { - logger.error(`Event is required`); - return res.status(400).json({ error: 'event is required' }); - } - - if (!body.event.startsWith('sdk_')) { - logger.error(`Wrong event name: ${body.event}`); - return res.status(400).json({ error: 'wrong event name' }); - } - - const toCheckEvents = ['sdk_rpc_request_done', 'sdk_rpc_request']; - const allowedMethods = [ - "eth_sendTransaction", - "wallet_switchEthereumChain", - "personal_sign", - "eth_signTypedData_v4", - "wallet_requestPermissions", - "metamask_connectSign" - ]; - - if (toCheckEvents.includes(body.event) && - (!body.method || !allowedMethods.includes(body.method))) { - return res.json({ success: true }); - } - - const channelId: string = body.id || 'sdk'; - let isExtensionEvent = body.from === 'extension'; - - if (typeof channelId !== 'string') { - logger.error(`Received event with invalid channelId: ${channelId}`, body); - return res.status(400).json({ status: 'error' }); - } - - if (channelId === 'sdk') { - isExtensionEvent = true; - } - - logger.debug( - `Received event /evt channelId=${channelId} isExtensionEvent=${isExtensionEvent}`, - body, - ); - - const userIdHash = crypto.createHash('sha1').update(channelId).digest('hex'); - - const event = { - userId: userIdHash, - event: body.event, - properties: { - userId: userIdHash, - ...body.properties, - }, - }; - - if (!event.properties.dappId) { - const newDappId = - event.properties.url && event.properties.url !== 'N/A' - ? event.properties.url - : event.properties.title || 'N/A'; - event.properties.dappId = newDappId; - logger.debug( - `event: ${event.event} - dappId missing - replacing with '${newDappId}'`, - event, - ); - } - - const propertiesToExclude: string[] = ['icon', 'originationInfo', 'id']; - - for (const property in body) { - if ( - Object.prototype.hasOwnProperty.call(body, property) && - body[property] && - !propertiesToExclude.includes(property) - ) { - event.properties[property] = body[property]; - } - } - - if (process.env.EVENTS_DEBUG_LOGS === 'true') { - logger.debug('Event object:', event); - } - - analytics.track(event, function (err: Error) { - if (process.env.EVENTS_DEBUG_LOGS === 'true') { - logger.info('Segment batch', JSON.stringify({ event }, null, 2)); - } else { - logger.info('Segment batch', { event }); - } - - if (err) { - logger.error('Segment error:', err); - } - }); - - return res.json({ success: true }); - } catch (error) { - return res.json({ error }); + try { + const { body } = req; + + if (!body.event) { + logger.error(`Event is required`); + return res.status(400).json({ error: 'event is required' }); + } + + if (!body.event.startsWith('sdk_')) { + logger.error(`Wrong event name: ${body.event}`); + return res.status(400).json({ error: 'wrong event name' }); + } + + const toCheckEvents = ['sdk_rpc_request_done', 'sdk_rpc_request']; + const allowedMethods = [ + 'eth_sendTransaction', + 'wallet_switchEthereumChain', + 'personal_sign', + 'eth_signTypedData_v4', + 'wallet_requestPermissions', + 'metamask_connectSign', + ]; + + if ( + toCheckEvents.includes(body.event) && + (!body.method || !allowedMethods.includes(body.method)) + ) { + return res.json({ success: true }); + } + + const channelId: string = body.id || 'sdk'; + let isExtensionEvent = body.from === 'extension'; + + if (typeof channelId !== 'string') { + logger.error(`Received event with invalid channelId: ${channelId}`, body); + return res.status(400).json({ status: 'error' }); + } + + if (channelId === 'sdk') { + isExtensionEvent = true; } + + logger.debug( + `Received event /evt channelId=${channelId} isExtensionEvent=${isExtensionEvent}`, + body, + ); + + const userIdHash = crypto.createHash('sha1').update(channelId).digest('hex'); + + const event = { + userId: userIdHash, + event: body.event, + properties: { + userId: userIdHash, + ...body.properties, + }, + }; + + if (!event.properties.dappId) { + const newDappId = + event.properties.url && event.properties.url !== 'N/A' + ? event.properties.url + : event.properties.title || 'N/A'; + event.properties.dappId = newDappId; + logger.debug(`event: ${event.event} - dappId missing - replacing with '${newDappId}'`, event); + } + + const propertiesToExclude: string[] = ['icon', 'originationInfo', 'id']; + + for (const property in body) { + if ( + Object.prototype.hasOwnProperty.call(body, property) && + body[property] && + !propertiesToExclude.includes(property) + ) { + event.properties[property] = body[property]; + } + } + + if (process.env.EVENTS_DEBUG_LOGS === 'true') { + logger.debug('Event object:', event); + } + + analytics.track(event, function (err: Error) { + if (process.env.EVENTS_DEBUG_LOGS === 'true') { + logger.info('Segment batch', JSON.stringify({ event }, null, 2)); + } else { + logger.info('Segment batch', { event }); + } + + if (err) { + logger.error('Segment error:', err); + } + }); + + return res.json({ success: true }); + } catch (error) { + return res.json({ error }); + } }); const port = process.env.PORT || 3001; app.listen(port, () => { - logger.info(`Analytics server listening on port ${port}`); + logger.info(`Analytics server listening on port ${port}`); }); export { app }; diff --git a/packages/analytics-server/src/logger.ts b/packages/analytics-server/src/logger.ts index 2de25afc1..7745ea723 100644 --- a/packages/analytics-server/src/logger.ts +++ b/packages/analytics-server/src/logger.ts @@ -3,17 +3,11 @@ import winston from 'winston'; export const createLogger = (isDevelopment: boolean) => { return winston.createLogger({ level: isDevelopment ? 'debug' : 'info', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json(), - ), + format: winston.format.combine(winston.format.timestamp(), winston.format.json()), transports: [ new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.simple(), - ), + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), }), ], }); -}; \ No newline at end of file +}; diff --git a/packages/analytics-server/tsconfig.eslint.json b/packages/analytics-server/tsconfig.eslint.json new file mode 100644 index 000000000..29c9ed9de --- /dev/null +++ b/packages/analytics-server/tsconfig.eslint.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", // Assuming analytics-server has a tsconfig.json + "include": [ + "src/**/*.ts", + "*.js" + // Include test files if applicable + ], + "compilerOptions": { + "noEmit": true + } +} \ No newline at end of file diff --git a/packages/devnext/src/pages/_app.tsx b/packages/devnext/src/pages/_app.tsx index 821e7fdd1..40af52391 100644 --- a/packages/devnext/src/pages/_app.tsx +++ b/packages/devnext/src/pages/_app.tsx @@ -29,7 +29,7 @@ const WithSDKConfig = ({ children }: { children: React.ReactNode }) => { debug={true} sdkOptions={{ communicationServerUrl: socketServer, - enableAnalytics: true, + enableAnalytics: false, // Display desktop tab instead of qrcode tab preferDesktop: false, infuraAPIKey, diff --git a/packages/sdk-communication-layer/e2e/sdk-comm.e2e.test.ts b/packages/sdk-communication-layer/e2e/sdk-comm.e2e.test.ts index 8f137498e..85294de29 100644 --- a/packages/sdk-communication-layer/e2e/sdk-comm.e2e.test.ts +++ b/packages/sdk-communication-layer/e2e/sdk-comm.e2e.test.ts @@ -59,6 +59,10 @@ export const getClient = ({ type, options }: { type: 'dApp' | 'wallet', options: reconnect: false, relayPersistence: true, analytics: true, + dappMetadata: { + name: `${type}-test`, + url: 'http://example.com', + }, logging: { eciesLayer: true, keyExchangeLayer: true, diff --git a/packages/sdk-communication-layer/jest.config.ts b/packages/sdk-communication-layer/jest.config.ts index c815a713a..77749d330 100644 --- a/packages/sdk-communication-layer/jest.config.ts +++ b/packages/sdk-communication-layer/jest.config.ts @@ -2,6 +2,15 @@ import baseConfig from '../../jest.config.base'; module.exports = { ...baseConfig, + moduleNameMapper: { + '^@metamask/analytics-client$': '/../analytics-client/src/index.ts', + '^@metamask/sdk-types$': '/../sdk-types/src/index.ts', + ...(baseConfig.moduleNameMapper || {}), + }, + transformIgnorePatterns: [ + '/node_modules/(?!(@metamask/analytics-client|@metamask/sdk-types)/)', + '\\\\.pnp\\\\.[^\\\\/]+$', + ], coveragePathIgnorePatterns: ['./src/types', '/index.ts$'], coverageThreshold: { global: { diff --git a/packages/sdk-communication-layer/package.json b/packages/sdk-communication-layer/package.json index d5db912e5..d26ae5663 100644 --- a/packages/sdk-communication-layer/package.json +++ b/packages/sdk-communication-layer/package.json @@ -16,7 +16,7 @@ "module": "dist/node/es/metamask-sdk-communication-layer.js", "browser": "dist/browser/es/metamask-sdk-communication-layer.js", "react-native": "dist/react-native/es/metamask-sdk-communication-layer.js", - "types": "dist/types/src/index.d.ts", + "types": "dist/types/packages/sdk-communication-layer/src/index.d.ts", "files": [ "/dist" ], @@ -29,7 +29,7 @@ "build:post-tsc": "echo 'N/A'", "build:pre-tsc": "echo 'N/A'", "size": "size-limit", - "clean": "rimraf ./dist", + "clean": "rimraf ./dist && rimraf tsconfig.build.tsbuildinfo", "lint": "yarn lint:eslint && yarn lint:misc --check", "lint:changelog": "../../scripts/validate-changelog.sh @metamask/sdk-communication-layer", "lint:eslint": "eslint . --cache --ext js,ts", @@ -46,6 +46,8 @@ "watch": "rollup -c --bundleConfigAsCjs -w" }, "dependencies": { + "@metamask/analytics-client": "workspace:^", + "@metamask/sdk-types": "workspace:^", "bufferutil": "^4.0.8", "date-fns": "^2.29.3", "debug": "^4.3.4", @@ -55,10 +57,12 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@lavamoat/allow-scripts": "^2.3.1", + "@metamask/analytics-client": "workspace:^", "@metamask/auto-changelog": "3.1.0", "@metamask/eslint-config": "^6.0.0", "@metamask/eslint-config-nodejs": "^6.0.0", "@metamask/eslint-config-typescript": "^6.0.0", + "@metamask/sdk-types": "workspace:^", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", @@ -71,7 +75,6 @@ "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", "concurrently": "^9.1.2", - "cross-fetch": "^4.0.0", "eciesjs": "^0.4.11", "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", @@ -99,10 +102,9 @@ "stream-browserify": "^3.0.0", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", - "typescript": "^5.6.3" + "typescript": "^4.3.5" }, "peerDependencies": { - "cross-fetch": "^4.0.0", "eciesjs": "*", "eventemitter2": "^6.4.9", "readable-stream": "^3.6.2", diff --git a/packages/sdk-communication-layer/src/RemoteCommunication.test.ts b/packages/sdk-communication-layer/src/RemoteCommunication.test.ts index 00d2e873b..1b68165fe 100644 --- a/packages/sdk-communication-layer/src/RemoteCommunication.test.ts +++ b/packages/sdk-communication-layer/src/RemoteCommunication.test.ts @@ -22,10 +22,18 @@ describe('RemoteCommunication', () => { beforeEach(() => { jest.clearAllMocks(); + const mockDappMetadata = { + name: 'Mock DApp', + url: 'http://mockdapp.com', + // Add other required fields if necessary, using mock values + source: 'test', + }; + remoteCommunicationInstance = new RemoteCommunication({ platformType: 'metamask-mobile', communicationLayerPreference: CommunicationLayerPreference.SOCKET, context: 'some-context', + dappMetadata: mockDappMetadata, }); }); diff --git a/packages/sdk-communication-layer/src/RemoteCommunication.ts b/packages/sdk-communication-layer/src/RemoteCommunication.ts index 655ad868b..257082602 100644 --- a/packages/sdk-communication-layer/src/RemoteCommunication.ts +++ b/packages/sdk-communication-layer/src/RemoteCommunication.ts @@ -1,5 +1,6 @@ import debug from 'debug'; import { EventEmitter2 } from 'eventemitter2'; +import { type OriginatorInfo } from '@metamask/sdk-types'; import packageJson from '../package.json'; import { ECIESProps } from './ECIES'; import { SocketService } from './SocketService'; @@ -31,7 +32,6 @@ import { DappMetadataWithSource } from './types/DappMetadata'; import { DisconnectOptions } from './types/DisconnectOptions'; import { EventType } from './types/EventType'; import { CommunicationLayerLoggingOptions } from './types/LoggingOptions'; -import { OriginatorInfo } from './types/OriginatorInfo'; import { PlatformType } from './types/PlatformType'; import { ServiceStatus } from './types/ServiceStatus'; import { @@ -53,7 +53,7 @@ export interface RemoteCommunicationProps { privateKey?: string; reconnect?: boolean; relayPersistence?: boolean; // Used by wallet to start the connection with relayPersistence and avoid the key exchange. - dappMetadata?: DappMetadataWithSource; + dappMetadata: DappMetadataWithSource; walletInfo?: WalletInfo; transports?: string[]; analytics?: boolean; @@ -89,7 +89,7 @@ export interface RemoteCommunicationState { originatorInfo?: OriginatorInfo; originatorInfoSent: boolean; reconnection: boolean; - dappMetadata?: DappMetadataWithSource; + dappMetadata: DappMetadataWithSource; communicationServerUrl: string; analyticsServerUrl: string; context: string; @@ -136,95 +136,70 @@ export class RemoteCommunication extends EventEmitter2 { // 2) If I am Dapp (isOriginator==true) then other side is MetaMask // Should not be set directly, use this.setConnectionStatus() instead to always emit events. _connectionStatus: ConnectionStatus.DISCONNECTED, - }; + } as RemoteCommunicationState; constructor(options: RemoteCommunicationProps) { super(); this._options = options; - const { - platformType, - communicationLayerPreference, - otherPublicKey, - reconnect, - walletInfo, - dappMetadata, - protocolVersion, - transports, - context, - relayPersistence, - ecies, - analytics = false, - storage, - sdkVersion, - communicationServerUrl = DEFAULT_SERVER_URL, - analyticsServerUrl = DEFAULT_ANALYTICS_SERVER_URL, - logging, - autoConnect = { + this.state = { + ...this.state, + otherPublicKey: options.otherPublicKey, + dappMetadata: options.dappMetadata, + walletInfo: options.walletInfo, + transports: options.transports, + platformType: options.platformType, + analytics: options?.analytics ?? false, + protocolVersion: options.protocolVersion ?? 1, + isOriginator: !options.otherPublicKey, + relayPersistence: options.relayPersistence, + communicationServerUrl: + options.communicationServerUrl ?? DEFAULT_SERVER_URL, + analyticsServerUrl: + options.analyticsServerUrl ?? DEFAULT_ANALYTICS_SERVER_URL, + context: options.context, + storageManager: options.storage?.storageManager, + sdkVersion: options.sdkVersion, + storageOptions: options.storage, + autoConnectOptions: options.autoConnect ?? { timeout: CHANNEL_MAX_WAITING_TIME, }, - } = options; - - this.state.otherPublicKey = otherPublicKey; - this.state.dappMetadata = dappMetadata; - this.state.walletInfo = walletInfo; - this.state.transports = transports; - this.state.platformType = platformType; - this.state.analytics = analytics; - this.state.protocolVersion = protocolVersion ?? 1; - this.state.isOriginator = !otherPublicKey; - this.state.relayPersistence = relayPersistence; - this.state.communicationServerUrl = communicationServerUrl; - this.state.analyticsServerUrl = analyticsServerUrl; - this.state.context = context; - this.state.terminated = false; - this.state.sdkVersion = sdkVersion; + debug: options.logging?.remoteLayer === true, + logging: options.logging, + }; this.setMaxListeners(50); this.setConnectionStatus(ConnectionStatus.DISCONNECTED); - if (storage?.duration) { + if (options.storage?.duration) { this.state.sessionDuration = DEFAULT_SESSION_TIMEOUT_MS; } - this.state.storageOptions = storage; - this.state.autoConnectOptions = autoConnect; - this.state.debug = logging?.remoteLayer === true; // Enable loggers early - if (logging?.remoteLayer === true) { + if (options.logging?.remoteLayer === true) { debug.enable('RemoteCommunication:Layer'); } - if (logging?.serviceLayer === true) { + if (options.logging?.serviceLayer === true) { debug.enable('SocketService:Layer'); } - if (logging?.eciesLayer === true) { + if (options.logging?.eciesLayer === true) { debug.enable('ECIES:Layer'); } - if (logging?.keyExchangeLayer === true) { + if (options.logging?.keyExchangeLayer === true) { debug.enable('KeyExchange:Layer'); } - this.state.logging = logging; - - if (storage?.storageManager) { - this.state.storageManager = storage.storageManager; - } - - logger.RemoteCommunication( - `[RemoteCommunication: constructor()] protocolVersion=${protocolVersion} relayPersistence=${relayPersistence} isOriginator=${this.state.isOriginator} communicationLayerPreference=${communicationLayerPreference} otherPublicKey=${otherPublicKey} reconnect=${reconnect}`, - ); - if (!this.state.isOriginator) { initSocketService({ - communicationLayerPreference, - otherPublicKey, - reconnect, - ecies, - communicationServerUrl, + communicationLayerPreference: CommunicationLayerPreference.SOCKET, + otherPublicKey: this.state.otherPublicKey, + reconnect: this._options.reconnect, + ecies: this._options.ecies, + communicationServerUrl: this.state.communicationServerUrl, instance: this, }); } diff --git a/packages/sdk-communication-layer/src/index.ts b/packages/sdk-communication-layer/src/index.ts index bfec4a105..b314cfead 100644 --- a/packages/sdk-communication-layer/src/index.ts +++ b/packages/sdk-communication-layer/src/index.ts @@ -1,4 +1,4 @@ -import { SendAnalytics, AnalyticsProps } from './Analytics'; +import { type OriginatorInfo, TrackingEvents } from '@metamask/sdk-types'; import { ECIES, ECIESProps } from './ECIES'; import { RemoteCommunication, @@ -23,13 +23,11 @@ import { EventType } from './types/EventType'; import { KeyInfo } from './types/KeyInfo'; import { CommunicationLayerLoggingOptions } from './types/LoggingOptions'; import { MessageType } from './types/MessageType'; -import { OriginatorInfo } from './types/OriginatorInfo'; import { PlatformType } from './types/PlatformType'; import { ServiceStatus } from './types/ServiceStatus'; import { WalletInfo } from './types/WalletInfo'; // eslint-disable-next-line @typescript-eslint/no-shadow import { StorageManager, StorageManagerProps } from './types/StorageManager'; -import { TrackingEvents } from './types/TrackingEvent'; import { KeyExchangeMessageType } from './types/KeyExchangeMessageType'; export type { @@ -50,7 +48,6 @@ export type { StorageManager, StorageManagerProps, WalletInfo, - AnalyticsProps, }; export { @@ -61,11 +58,10 @@ export { DEFAULT_SESSION_TIMEOUT_MS, ECIES, EventType, + KeyExchangeMessageType, MessageType, PlatformType, RemoteCommunication, - KeyExchangeMessageType, - SendAnalytics, SocketService, TrackingEvents, }; diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts index c317ff946..a79d8ca0a 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts @@ -1,12 +1,12 @@ import { v4 as uuidv4 } from 'uuid'; +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; import { logger } from '../../../utils/logger'; import { RemoteCommunication } from '../../../RemoteCommunication'; import { ConnectionStatus } from '../../../types/ConnectionStatus'; import { DisconnectOptions } from '../../../types/DisconnectOptions'; import { MessageType } from '../../../types/MessageType'; import { encryptAndSendMessage } from '../../SocketService/MessageHandlers'; -import { SendAnalytics } from '../../../Analytics'; -import { TrackingEvents } from '../../../types/TrackingEvent'; /** * Handles the disconnection process for a RemoteCommunication instance Depending on the provided options, it can terminate the connection and clear related configurations or simply disconnect. @@ -64,7 +64,7 @@ export async function disconnect({ ); resolve(true); }) - .catch((error) => { + .catch((error: unknown) => { reject(error); }); } diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initSocketService.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initSocketService.ts index d5bbbb662..48af75f3c 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initSocketService.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initSocketService.ts @@ -1,11 +1,11 @@ +import { type OriginatorInfo } from '@metamask/sdk-types'; import packageJson from '../../../../package.json'; -import { ECIESProps } from '../../../ECIES'; +import { type ECIESProps } from '../../../ECIES'; import { RemoteCommunication } from '../../../RemoteCommunication'; import { SocketService } from '../../../SocketService'; import { DEFAULT_SERVER_URL } from '../../../config'; import { CommunicationLayerPreference } from '../../../types/CommunicationLayerPreference'; import { EventType } from '../../../types/EventType'; -import { OriginatorInfo } from '../../../types/OriginatorInfo'; import { logger } from '../../../utils/logger'; import { handleAuthorizedEvent, @@ -111,7 +111,7 @@ export function initSocketService({ title, source: state.dappMetadata?.source, dappId, - icon: state.dappMetadata?.iconUrl || state.dappMetadata?.base64Icon, + icon: state.dappMetadata?.iconUrl, platform: state.platformType, apiVersion: packageJson.version, connector: state.dappMetadata?.connector, @@ -125,14 +125,8 @@ export function initSocketService({ [EventType.AUTHORIZED]: handleAuthorizedEvent(instance), [EventType.MESSAGE]: handleMessageEvent(instance), [EventType.CHANNEL_PERSISTENCE]: handleFullPersistenceEvent(instance), - [EventType.CLIENTS_CONNECTED]: handleClientsConnectedEvent( - instance, - communicationLayerPreference, - ), - [EventType.KEYS_EXCHANGED]: handleKeysExchangedEvent( - instance, - communicationLayerPreference, - ), + [EventType.CLIENTS_CONNECTED]: handleClientsConnectedEvent(instance), + [EventType.KEYS_EXCHANGED]: handleKeysExchangedEvent(instance), [EventType.SOCKET_DISCONNECTED]: handleSocketDisconnectedEvent(instance), [EventType.SOCKET_RECONNECT]: handleSocketReconnectEvent(instance), [EventType.CLIENTS_DISCONNECTED]: handleClientsDisconnectedEvent(instance), diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts index 74fe1083f..260942c35 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts @@ -1,9 +1,9 @@ // packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/connectToChannel.ts import { validate } from 'uuid'; +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; import { RemoteCommunicationState } from '../../../RemoteCommunication'; import { EventType } from '../../../types/EventType'; -import { SendAnalytics } from '../../../Analytics'; -import { TrackingEvents } from '../../../types/TrackingEvent'; import { logger } from '../../../utils/logger'; import packageJson from '../../../../package.json'; @@ -57,7 +57,7 @@ export async function rejectChannel({ walletVersion: state.walletInfo?.version, }, state.analyticsServerUrl, - ).catch((error) => { + ).catch((error: unknown) => { console.error(`rejectChannel:: Error emitting analytics event`, error); }); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts index ffe6697e7..ec5e9061f 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts @@ -1,12 +1,12 @@ +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; +import packageJson from '../../../../package.json'; import { RemoteCommunication } from '../../../RemoteCommunication'; -import { CommunicationLayerPreference } from '../../../types/CommunicationLayerPreference'; import { EventType } from '../../../types/EventType'; -import { SendAnalytics } from '../../../Analytics'; -import packageJson from '../../../../package.json'; import { logger } from '../../../utils/logger'; import { handleClientsConnectedEvent } from './handleClientsConnectedEvent'; -jest.mock('../../../Analytics', () => ({ +jest.mock('@metamask/analytics-client', () => ({ SendAnalytics: jest.fn().mockResolvedValue(undefined), })); @@ -26,16 +26,19 @@ describe.skip('handleClientsConnectedEvent', () => { instance = { state: { debug: true, + context: 'mockContext', channelId: 'testChannel', analytics: true, + sdkVersion: '1.0', + walletInfo: { version: '1.0' }, communicationLayer: { getKeyInfo: mockGetKeyInfo, }, - sdkVersion: '1.0', originatorInfo: { someKey: 'someValue', }, - communicationServerUrl: 'http://mock-url.com', + analyticsServerUrl: 'http://mock-url.com', + isOriginator: false, }, emit: mockEmit, } as unknown as RemoteCommunication; @@ -46,10 +49,7 @@ describe.skip('handleClientsConnectedEvent', () => { }); it('should log the event details', () => { - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); handler(); expect(spyLogger).toHaveBeenCalledWith( @@ -58,37 +58,30 @@ describe.skip('handleClientsConnectedEvent', () => { }); it('should send analytics data if analytics tracking is enabled', async () => { - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); await handler(); expect(SendAnalytics).toHaveBeenCalledWith( expect.objectContaining({ + event: TrackingEvents.RECONNECT, id: 'testChannel', - commLayer: CommunicationLayerPreference.SOCKET, sdkVersion: '1.0', commLayerVersion: packageJson.version, + walletVersion: '1.0', + someKey: 'someValue', }), 'http://mock-url.com', ); }); it('should update the state of the instance correctly', () => { - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); handler(); expect(instance.state.clientsConnected).toBe(true); expect(instance.state.originatorInfoSent).toBe(false); }); it('should emit CLIENTS_CONNECTED event', () => { - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); handler(); expect(mockEmit).toHaveBeenCalledWith(EventType.CLIENTS_CONNECTED); }); @@ -97,10 +90,7 @@ describe.skip('handleClientsConnectedEvent', () => { (SendAnalytics as jest.Mock).mockRejectedValueOnce( new Error('Failed to send analytics'), ); - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); await handler(); expect(console.error).toHaveBeenCalledWith( 'Cannot send analytics', @@ -110,33 +100,21 @@ describe.skip('handleClientsConnectedEvent', () => { it('should not send analytics data if analytics tracking is disabled', async () => { instance.state.analytics = false; - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); await handler(); expect(SendAnalytics).not.toHaveBeenCalled(); }); it('should handle missing channelId gracefully', async () => { delete instance.state.channelId; - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); await handler(); - expect(SendAnalytics).toHaveBeenCalledWith( - expect.objectContaining({ id: '' }), - 'http://mock-url.com', - ); + expect(SendAnalytics).not.toHaveBeenCalled(); }); it('should handle missing communicationLayer gracefully', () => { delete instance.state.communicationLayer; - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); handler(); // No errors should be thrown expect(mockEmit).toHaveBeenCalledWith(EventType.CLIENTS_CONNECTED); @@ -144,18 +122,20 @@ describe.skip('handleClientsConnectedEvent', () => { it('should send analytics data correctly if originatorInfo is missing', async () => { delete instance.state.originatorInfo; - const handler = handleClientsConnectedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleClientsConnectedEvent(instance); await handler(); expect(SendAnalytics).toHaveBeenCalledWith( expect.objectContaining({ - commLayer: CommunicationLayerPreference.SOCKET, + event: TrackingEvents.RECONNECT, + id: 'testChannel', sdkVersion: '1.0', commLayerVersion: packageJson.version, + walletVersion: '1.0', }), 'http://mock-url.com', ); + const receivedParams = (SendAnalytics as jest.Mock).mock.calls[0][0]; + expect(receivedParams).not.toHaveProperty('originatorInfo'); + expect(receivedParams).not.toHaveProperty('someKey'); }); }); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts index 5f00a51f3..7dd5b9148 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts @@ -1,10 +1,9 @@ -import { logger } from '../../../utils/logger'; +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; import packageJson from '../../../../package.json'; -import { SendAnalytics } from '../../../Analytics'; import { RemoteCommunication } from '../../../RemoteCommunication'; -import { CommunicationLayerPreference } from '../../../types/CommunicationLayerPreference'; import { EventType } from '../../../types/EventType'; -import { TrackingEvents } from '../../../types/TrackingEvent'; +import { logger } from '../../../utils/logger'; /** * Creates and returns an event handler function for the "CLIENTS_CONNECTED" event. This handler function manages the connected clients for a given RemoteCommunication instance. @@ -16,13 +15,9 @@ import { TrackingEvents } from '../../../types/TrackingEvent'; * 4. The "CLIENTS_CONNECTED" event is emitted to inform other parts of the system about the successful connection of clients. * * @param instance The instance of RemoteCommunication for which the event handler function is being created. - * @param communicationLayerPreference The preferred communication layer used for this connection. * @returns A function that acts as the event handler for the "CLIENTS_CONNECTED" event. */ -export function handleClientsConnectedEvent( - instance: RemoteCommunication, - communicationLayerPreference: CommunicationLayerPreference, -) { +export function handleClientsConnectedEvent(instance: RemoteCommunication) { return () => { const { state } = instance; // Propagate the event to manage different loading states on the ui. @@ -32,22 +27,20 @@ export function handleClientsConnectedEvent( } keysExchanged=${state.communicationLayer?.getKeyInfo()?.keysExchanged}`, ); - if (state.analytics) { - const requestEvent = state.isOriginator - ? TrackingEvents.REQUEST - : TrackingEvents.REQUEST_MOBILE; + if (state.analytics && state.channelId) { SendAnalytics( { - id: state.channelId ?? '', - event: state.reconnection ? TrackingEvents.RECONNECT : requestEvent, + id: state.channelId, + event: state.isOriginator + ? TrackingEvents.REQUEST + : TrackingEvents.RECONNECT, ...state.originatorInfo, - commLayer: communicationLayerPreference, sdkVersion: state.sdkVersion, walletVersion: state.walletInfo?.version, commLayerVersion: packageJson.version, }, state.analyticsServerUrl, - ).catch((err) => { + ).catch((err: unknown) => { console.error(`Cannot send analytics`, err); }); } diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsDisconnectedEvent.test.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsDisconnectedEvent.test.ts index a3865882f..d6f7f697f 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsDisconnectedEvent.test.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsDisconnectedEvent.test.ts @@ -1,4 +1,4 @@ -import { SendAnalytics } from '../../../Analytics'; +import { SendAnalytics } from '@metamask/analytics-client'; import { RemoteCommunication } from '../../../RemoteCommunication'; import { ConnectionStatus } from '../../../types/ConnectionStatus'; import { EventType } from '../../../types/EventType'; @@ -9,7 +9,7 @@ jest.mock('../../../../package.json', () => ({ version: 'mockVersion', })); -jest.mock('../../../Analytics', () => ({ +jest.mock('@metamask/analytics-client', () => ({ SendAnalytics: jest.fn(() => Promise.resolve()), })); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.test.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.test.ts index c011fd048..cb55f7dfa 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.test.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.test.ts @@ -1,14 +1,13 @@ -import { SendAnalytics } from '../../../Analytics'; +import { SendAnalytics } from '@metamask/analytics-client'; +import { OriginatorInfo } from '@metamask/sdk-types'; import { RemoteCommunication } from '../../../RemoteCommunication'; -import { CommunicationLayerPreference } from '../../../types/CommunicationLayerPreference'; import { ConnectionStatus } from '../../../types/ConnectionStatus'; import { MessageType } from '../../../types/MessageType'; -import { OriginatorInfo } from '../../../types/OriginatorInfo'; -import { setLastActiveDate } from '../StateManger'; import { logger } from '../../../utils/logger'; +import { setLastActiveDate } from '../StateManger'; import { handleKeysExchangedEvent } from './handleKeysExchangedEvent'; -jest.mock('../../../Analytics', () => { +jest.mock('@metamask/analytics-client', () => { return { SendAnalytics: jest.fn().mockResolvedValue(undefined), }; @@ -50,10 +49,7 @@ describe('handleKeysExchangedEvent', () => { }); it('should log diagnostic information', () => { - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -64,10 +60,7 @@ describe('handleKeysExchangedEvent', () => { }); it('should update the instance connection status to LINKED', () => { - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -80,10 +73,7 @@ describe('handleKeysExchangedEvent', () => { }); it('should send a READY message if the instance is not the originator', () => { - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: false, originatorInfo: {} as OriginatorInfo, @@ -96,10 +86,7 @@ describe('handleKeysExchangedEvent', () => { it('should send an ORIGINATOR_INFO message if the instance is the originator and originatorInfo has not been sent', () => { instance.state.originatorInfoSent = false; - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -115,10 +102,7 @@ describe('handleKeysExchangedEvent', () => { }); it('should attempt to send analytics when analytics is enabled', () => { - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -130,10 +114,7 @@ describe('handleKeysExchangedEvent', () => { it('should not attempt to send analytics when analytics is disabled', () => { instance.state.analytics = false; - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -144,10 +125,7 @@ describe('handleKeysExchangedEvent', () => { }); it('should set the last active date', () => { - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -159,10 +137,7 @@ describe('handleKeysExchangedEvent', () => { it('should log an error when sending analytics fails', async () => { (SendAnalytics as jest.Mock).mockRejectedValue(new Error('Mock error')); - const handler = handleKeysExchangedEvent( - instance, - CommunicationLayerPreference.SOCKET, - ); + const handler = handleKeysExchangedEvent(instance); handler({ isOriginator: true, originatorInfo: {} as OriginatorInfo, @@ -171,7 +146,7 @@ describe('handleKeysExchangedEvent', () => { await expect(SendAnalytics).rejects.toThrow('Mock error'); expect(console.error).toHaveBeenCalledWith( - 'Cannot send analytics', + 'Failed to send analytics key exchanged event', expect.anything(), ); }); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts index 22a6d767e..6d1daf9e3 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts @@ -1,13 +1,11 @@ +import { SendAnalytics } from '@metamask/analytics-client'; +import { type OriginatorInfo, TrackingEvents } from '@metamask/sdk-types'; import packageJson from '../../../../package.json'; -import { SendAnalytics } from '../../../Analytics'; import { DEFAULT_SESSION_TIMEOUT_MS } from '../../../config'; import { RemoteCommunication } from '../../../RemoteCommunication'; import { ChannelConfig } from '../../../types/ChannelConfig'; -import { CommunicationLayerPreference } from '../../../types/CommunicationLayerPreference'; import { ConnectionStatus } from '../../../types/ConnectionStatus'; import { MessageType } from '../../../types/MessageType'; -import { OriginatorInfo } from '../../../types/OriginatorInfo'; -import { TrackingEvents } from '../../../types/TrackingEvent'; import { logger } from '../../../utils/logger'; import { setLastActiveDate } from '../StateManger'; @@ -29,13 +27,9 @@ import { setLastActiveDate } from '../StateManger'; * b. The state variable `originatorInfoSent` is updated to indicate that the originator information has been transmitted. * * @param instance The `RemoteCommunication` instance for which the event handler function is being created. - * @param communicationLayerPreference The communication layer preference, used for analytics. * @returns A function that acts as the event handler for the "keys_exchanged" event, expecting a message containing details about the key exchange. */ -export function handleKeysExchangedEvent( - instance: RemoteCommunication, - communicationLayerPreference: CommunicationLayerPreference, -) { +export function handleKeysExchangedEvent(instance: RemoteCommunication) { return (message: { isOriginator: boolean; originatorInfo: OriginatorInfo; @@ -77,13 +71,12 @@ export function handleKeysExchangedEvent( : TrackingEvents.CONNECTED_MOBILE, ...state.originatorInfo, sdkVersion: state.sdkVersion, - commLayer: communicationLayerPreference, commLayerVersion: packageJson.version, walletVersion: state.walletInfo?.version, }, state.analyticsServerUrl, - ).catch((err) => { - console.error(`Cannot send analytics`, err); + ).catch((err: unknown) => { + console.error('Failed to send analytics key exchanged event', err); }); } diff --git a/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts b/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts index f91f84391..a907b80ac 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts @@ -1,3 +1,5 @@ +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; import { SocketService } from '../../../SocketService'; import { EventType } from '../../../types/EventType'; import { KeyExchangeMessageType } from '../../../types/KeyExchangeMessageType'; @@ -6,8 +8,6 @@ import { ChannelConfig } from '../../../types/ChannelConfig'; import { DEFAULT_SESSION_TIMEOUT_MS } from '../../../config'; import { ConnectionStatus } from '../../../types/ConnectionStatus'; import { logger } from '../../../utils/logger'; -import { SendAnalytics } from '../../../Analytics'; -import { TrackingEvents } from '../../../types/TrackingEvent'; import packageJson from '../../../../package.json'; @@ -79,8 +79,8 @@ export const handleJoinChannelResults = async ( .sendMessage({ type: KeyExchangeMessageType.KEY_HANDSHAKE_ACK, }) - .catch((err) => { - console.error(err); + .catch((err: unknown) => { + console.error(`SocketService.handleJoinChannelResult() Error: ${err}`); }); instance.state.socket?.emit(MessageType.PING, { @@ -110,12 +110,11 @@ export const handleJoinChannelResults = async ( : TrackingEvents.CONNECTED_MOBILE, ...instance.remote.state.originatorInfo, sdkVersion: instance.remote.state.sdkVersion, - commLayer: instance.state.communicationLayerPreference, commLayerVersion: packageJson.version, walletVersion: instance.remote.state.walletInfo?.version, }, remote.state.analyticsServerUrl, - ).catch((err) => { + ).catch((err: unknown) => { console.error(`Cannot send analytics`, err); }); } diff --git a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts index 145ff29a8..097b3106a 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts @@ -1,8 +1,8 @@ -import { SendAnalytics } from '../../../Analytics'; +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; import { SocketService } from '../../../SocketService'; import { ConnectionStatus } from '../../../types/ConnectionStatus'; import { EventType } from '../../../types/EventType'; -import { TrackingEvents } from '../../../types/TrackingEvent'; import { logger } from '../../../utils/logger'; import packageJson from '../../../../package.json'; @@ -39,16 +39,12 @@ export function handleChannelRejected( event: TrackingEvents.REJECTED, ...instance.remote.state.originatorInfo, sdkVersion: instance.remote.state.sdkVersion, - commLayer: instance.state.communicationLayerPreference, commLayerVersion: packageJson.version, walletVersion: instance.remote.state.walletInfo?.version, }, instance.remote.state.analyticsServerUrl, - ).catch((error) => { - console.error( - `handleChannelRejected:: Error emitting analytics event`, - error, - ); + ).catch((_error: unknown) => { + // ignore error }); // Terminate the channel diff --git a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts index bd46c4c5a..ee7d26859 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts @@ -1,11 +1,11 @@ +import { SendAnalytics } from '@metamask/analytics-client'; +import { TrackingEvents } from '@metamask/sdk-types'; import packageJson from '../../../../package.json'; -import { SendAnalytics } from '../../../Analytics'; import { SocketService } from '../../../SocketService'; import { EventType } from '../../../types/EventType'; import { InternalEventType } from '../../../types/InternalEventType'; import { KeyExchangeMessageType } from '../../../types/KeyExchangeMessageType'; import { MessageType } from '../../../types/MessageType'; -import { TrackingEvents } from '../../../types/TrackingEvent'; import { logger } from '../../../utils/logger'; import { lcLogguedRPCs } from '../MessageHandlers'; @@ -228,7 +228,7 @@ export function handleMessage(instance: SocketService, channelId: string) { }, }, instance.remote.state.analyticsServerUrl, - ).catch((err) => { + ).catch((err: unknown) => { console.error(`Cannot send analytics`, err); }); } diff --git a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts index b1c665459..dbcd4873f 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts @@ -1,7 +1,7 @@ -import { SendAnalytics } from '../../../Analytics'; +import { TrackingEvents } from '@metamask/sdk-types'; +import { SendAnalytics } from '@metamask/analytics-client'; import { SocketService } from '../../../SocketService'; import { CommunicationLayerMessage } from '../../../types/CommunicationLayerMessage'; -import { TrackingEvents } from '../../../types/TrackingEvent'; import { logger } from '../../../utils/logger'; import { handleKeyHandshake, validateKeyExchange } from '../KeysManager'; import { encryptAndSendMessage } from './encryptAndSendMessage'; @@ -84,7 +84,7 @@ export async function handleSendMessage( }, }, instance.remote.state.analyticsServerUrl, - ).catch((err) => { + ).catch((err: unknown) => { console.error(`[handleSendMessage] Cannot send analytics`, err); }); } diff --git a/packages/sdk-communication-layer/src/types/CommunicationLayerMessage.ts b/packages/sdk-communication-layer/src/types/CommunicationLayerMessage.ts index 056d73cc2..14ccbe688 100644 --- a/packages/sdk-communication-layer/src/types/CommunicationLayerMessage.ts +++ b/packages/sdk-communication-layer/src/types/CommunicationLayerMessage.ts @@ -1,6 +1,6 @@ +import { type OriginatorInfo } from '@metamask/sdk-types'; import { KeyExchangeMessageType } from './KeyExchangeMessageType'; import { MessageType } from './MessageType'; -import { OriginatorInfo } from './OriginatorInfo'; import { WalletInfo } from './WalletInfo'; export interface CommunicationLayerMessage { diff --git a/packages/sdk-communication-layer/src/types/OriginatorInfo.ts b/packages/sdk-communication-layer/src/types/OriginatorInfo.ts deleted file mode 100644 index a9adb0414..000000000 --- a/packages/sdk-communication-layer/src/types/OriginatorInfo.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface OriginatorInfo { - url: string; - title: string; - platform: string; - dappId: string; - icon?: string; - scheme?: string; - source?: string; - apiVersion?: string; - connector?: string; -} diff --git a/packages/sdk-communication-layer/src/types/RemoteMessage.ts b/packages/sdk-communication-layer/src/types/RemoteMessage.ts index d643f7843..afde8b1ff 100644 --- a/packages/sdk-communication-layer/src/types/RemoteMessage.ts +++ b/packages/sdk-communication-layer/src/types/RemoteMessage.ts @@ -1,5 +1,5 @@ +import { type OriginatorInfo } from '@metamask/sdk-types'; // Use types package import { MessageType } from './MessageType'; -import { OriginatorInfo } from './OriginatorInfo'; export interface RemoteMessage { type: MessageType; diff --git a/packages/sdk-communication-layer/src/types/ServiceStatus.ts b/packages/sdk-communication-layer/src/types/ServiceStatus.ts index 4fd808c35..709061d81 100644 --- a/packages/sdk-communication-layer/src/types/ServiceStatus.ts +++ b/packages/sdk-communication-layer/src/types/ServiceStatus.ts @@ -1,7 +1,7 @@ +import { type OriginatorInfo } from '@metamask/sdk-types'; import { ChannelConfig } from './ChannelConfig'; import { ConnectionStatus } from './ConnectionStatus'; import { KeyInfo } from './KeyInfo'; -import { OriginatorInfo } from './OriginatorInfo'; export interface ServiceStatus { keyInfo?: KeyInfo; diff --git a/packages/sdk-communication-layer/src/types/TrackingEvent.ts b/packages/sdk-communication-layer/src/types/TrackingEvent.ts deleted file mode 100644 index cbebef67a..000000000 --- a/packages/sdk-communication-layer/src/types/TrackingEvent.ts +++ /dev/null @@ -1,17 +0,0 @@ -export enum TrackingEvents { - REQUEST = 'sdk_connect_request_started', - REQUEST_MOBILE = 'sdk_connect_request_started_mobile', - RECONNECT = 'sdk_reconnect_request_started', - CONNECTED = 'sdk_connection_established', - CONNECTED_MOBILE = 'sdk_connection_established_mobile', - AUTHORIZED = 'sdk_connection_authorized', - REJECTED = 'sdk_connection_rejected', - TERMINATED = 'sdk_connection_terminated', - DISCONNECTED = 'sdk_disconnected', - SDK_USE_EXTENSION = 'sdk_use_extension', - SDK_RPC_REQUEST = 'sdk_rpc_request', - SDK_RPC_REQUEST_RECEIVED = 'sdk_rpc_request_received', - SDK_RPC_REQUEST_DONE = 'sdk_rpc_request_done', - SDK_EXTENSION_UTILIZED = 'sdk_extension_utilized', - SDK_USE_INAPP_BROWSER = 'sdk_use_inapp_browser', -} diff --git a/packages/sdk-communication-layer/tsconfig.build.json b/packages/sdk-communication-layer/tsconfig.build.json index 4e0057bb1..1ef972736 100644 --- a/packages/sdk-communication-layer/tsconfig.build.json +++ b/packages/sdk-communication-layer/tsconfig.build.json @@ -2,5 +2,6 @@ "extends": "./tsconfig.json", "compilerOptions": {}, "include": ["./src", "package.json"], - "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"], + "references": [{ "path": "../sdk-types" }, { "path": "../analytics-client" }] } diff --git a/packages/sdk-communication-layer/tsconfig.json b/packages/sdk-communication-layer/tsconfig.json index 39203a880..4270acc1f 100644 --- a/packages/sdk-communication-layer/tsconfig.json +++ b/packages/sdk-communication-layer/tsconfig.json @@ -1,10 +1,11 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": "./", + "composite": true, "declaration": true, "declarationMap": true, "sourceMap": true, - "strict": true, "outDir": "dist", "target": "es6", "esModuleInterop": true, @@ -16,6 +17,7 @@ "skipLibCheck": true, "types": ["node"] }, - "include": ["./src"], - "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] + "include": ["./src", "./package.json"], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"], + "references": [{ "path": "../sdk-types" }, { "path": "../analytics-client" }] } diff --git a/packages/sdk-communication-layer/tsconfig.test.json b/packages/sdk-communication-layer/tsconfig.test.json index 8dec60e64..2f3ec6c92 100644 --- a/packages/sdk-communication-layer/tsconfig.test.json +++ b/packages/sdk-communication-layer/tsconfig.test.json @@ -1,5 +1,7 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { + "baseUrl": ".", "outDir": "dist", "declaration": true, "declarationMap": true, diff --git a/packages/sdk-install-modal-web/.eslintrc.js b/packages/sdk-install-modal-web/.eslintrc.js index 2c588227f..99c1c8611 100644 --- a/packages/sdk-install-modal-web/.eslintrc.js +++ b/packages/sdk-install-modal-web/.eslintrc.js @@ -11,6 +11,8 @@ module.exports = { ignorePatterns: [ '.eslintrc.js', 'dist', + 'build', + 'wwww', 'rollup.config.js', '**/coverage/**', 'postcss.config.js', diff --git a/packages/sdk-install-modal-web/.gitignore b/packages/sdk-install-modal-web/.gitignore index c79078fdb..055618da9 100644 --- a/packages/sdk-install-modal-web/.gitignore +++ b/packages/sdk-install-modal-web/.gitignore @@ -17,6 +17,7 @@ lib-cov coverage junit.xml +wwww # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/packages/sdk-socket-server-next/package.json b/packages/sdk-socket-server-next/package.json index d886a9dcd..034a73321 100644 --- a/packages/sdk-socket-server-next/package.json +++ b/packages/sdk-socket-server-next/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/sdk-socket-server-scale", - "version": "2.10.1", + "version": "2.11.0", "private": true, "description": "", "homepage": "https://github.com/MetaMask/metamask-sdk#readme", diff --git a/packages/sdk-types/.eslintrc.js b/packages/sdk-types/.eslintrc.js new file mode 100644 index 000000000..6160950e6 --- /dev/null +++ b/packages/sdk-types/.eslintrc.js @@ -0,0 +1,49 @@ +const path = require('path'); + +/** + * @type {import('eslint').Linter.Config} + */ +module.exports = { + extends: ['@metamask/eslint-config-typescript', '../../.eslintrc.js'], + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + project: [path.resolve(__dirname, 'tsconfig.eslint.json')], + }, + ignorePatterns: [ + '.prettierrc.js', + '**/.eslintrc.js', + '**/dist*/', + ], + overrides: [ + { + files: ['**/*.ts'], + rules: { + // Add specific overrides if needed + "@typescript-eslint/naming-convention": [ + "error", + // Allow PascalCase for classes, interfaces, types, enums + { + "selector": "typeLike", + "format": ["PascalCase"] + }, + // Allow camelCase, PascalCase, UPPER_CASE for variables + { + "selector": "variable", + "format": ["camelCase", "PascalCase", "UPPER_CASE"] + }, + // Allow camelCase, PascalCase for functions + { + "selector": "function", + "format": ["camelCase", "PascalCase"] + }, + // Allow SCREAMING_SNAKE_CASE for enum members + { + "selector": "enumMember", + "format": ["PascalCase", "UPPER_CASE"] // Add UPPER_CASE (alias for SCREAMING_SNAKE_CASE) + } + ] + }, + }, + ], +}; \ No newline at end of file diff --git a/packages/sdk-types/README.md b/packages/sdk-types/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/sdk-types/package.json b/packages/sdk-types/package.json new file mode 100644 index 000000000..d8d607134 --- /dev/null +++ b/packages/sdk-types/package.json @@ -0,0 +1,60 @@ +{ + "name": "@metamask/sdk-types", + "version": "0.1.0", + "description": "Shared TypeScript types for MetaMask SDK packages", + "homepage": "https://github.com/MetaMask/metamask-sdk#readme", + "bugs": { + "url": "https://github.com/MetaMask/metamask-sdk/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/metamask-sdk.git", + "directory": "packages/sdk-types" + }, + "license": "MIT", + "author": "MetaMask", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "build": "yarn build:clean && tsc --project tsconfig.json", + "build:types": "tsc --project tsconfig.json --emitDeclarationOnly --outDir dist/types", + "build:clean": "rimraf ./dist && rimraf tsconfig.tsbuildinfo", + "clean": "rimraf ./dist && rimraf tsconfig.tsbuildinfo", + "lint": "yarn lint:eslint && yarn lint:misc --check", + "lint:eslint": "eslint src --cache --ext js,ts", + "lint:changelog": "echo na", + "test": "echo N/A", + "test:unit": "echo N/A", + "test:ci": "echo N/A", + "allow-scripts": "echo N/a", + "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", + "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path ../../.gitignore", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@lavamoat/allow-scripts": "^2.3.1", + "@metamask/eslint-config": "^12.1.0", + "@metamask/eslint-config-nodejs": "^12.1.0", + "@metamask/eslint-config-typescript": "^12.1.0", + "@types/node": "^18.11.18", + "@typescript-eslint/eslint-plugin": "^5.48.1", + "@typescript-eslint/parser": "^5.48.1", + "eslint": "^8.31.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsdoc": "^39.6.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.2.1", + "prettier": "^2.8.8", + "rimraf": "^3.0.2", + "typescript": "^4.3.5" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "packageManager": "yarn@3.5.1" +} diff --git a/packages/sdk-types/src/analytics/TrackingEvents.ts b/packages/sdk-types/src/analytics/TrackingEvents.ts new file mode 100644 index 000000000..3da0faf43 --- /dev/null +++ b/packages/sdk-types/src/analytics/TrackingEvents.ts @@ -0,0 +1,29 @@ +/** + * Object defining the names of trackable analytics events within the SDK. + * Using 'as const' for better type inference and tree-shaking. + */ +export const TrackingEvents = { + REQUEST: 'sdk_connect_request_started', + REQUEST_MOBILE: 'sdk_connect_request_started_mobile', + RECONNECT: 'sdk_reconnect_request_started', + CONNECTED: 'sdk_connection_established', + CONNECTED_MOBILE: 'sdk_connection_established_mobile', + AUTHORIZED: 'sdk_connection_authorized', + REJECTED: 'sdk_connection_rejected', + TERMINATED: 'sdk_connection_terminated', + DISCONNECTED: 'sdk_disconnected', + SDK_USE_EXTENSION: 'sdk_use_extension', + SDK_RPC_REQUEST: 'sdk_rpc_request', + SDK_RPC_REQUEST_RECEIVED: 'sdk_rpc_request_received', + SDK_RPC_REQUEST_DONE: 'sdk_rpc_request_done', + SDK_EXTENSION_UTILIZED: 'sdk_extension_utilized', + SDK_USE_INAPP_BROWSER: 'sdk_use_inapp_browser', +} as const; + +/** + * Union type derived from the values of the TrackingEvents object. + * Use this type for function parameters or variable annotations + * where any tracking event value is expected. + */ +export type TrackingEvent = + (typeof TrackingEvents)[keyof typeof TrackingEvents]; diff --git a/packages/sdk-types/src/index.ts b/packages/sdk-types/src/index.ts new file mode 100644 index 000000000..d2d0a097c --- /dev/null +++ b/packages/sdk-types/src/index.ts @@ -0,0 +1,7 @@ +// Index file for @metamask/sdk-types +// Exports will be added as types are moved here. + +export type { OriginatorInfo } from './originator/OriginatorInfo'; +export { TrackingEvents, type TrackingEvent } from './analytics/TrackingEvents'; // Export enum + +export {}; // Add an empty export to satisfy module requirements initially diff --git a/packages/sdk-types/src/originator/OriginatorInfo.ts b/packages/sdk-types/src/originator/OriginatorInfo.ts new file mode 100644 index 000000000..04ee4ef9f --- /dev/null +++ b/packages/sdk-types/src/originator/OriginatorInfo.ts @@ -0,0 +1,23 @@ +/** + * Defines the metadata for the originator (DApp) sending analytics or requesting connection. + */ +export type OriginatorInfo = { + /** The canonical URL of the DApp */ + url: string; + /** The display name of the DApp */ + title: string; + /** The platform identifier (e.g., 'web', 'react-native') */ + platform: string; + /** A unique identifier for the DApp, often the hostname */ + dappId: string; + /** Optional URL to the DApp's icon */ + icon?: string; + /** Optional URL scheme used by the DApp (e.g., for deep linking) */ + scheme?: string; + /** Optional source identifier (e.g., 'qrcode', 'deeplink') */ + source?: string; + /** Optional API version used by the DApp */ + apiVersion?: string; + /** Optional identifier for the specific connector/library used (e.g., '@metamask/sdk-react-native') */ + connector?: string; +}; diff --git a/packages/sdk-types/tsconfig.eslint.json b/packages/sdk-types/tsconfig.eslint.json new file mode 100644 index 000000000..f4add8000 --- /dev/null +++ b/packages/sdk-types/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "*.js"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/packages/sdk-types/tsconfig.json b/packages/sdk-types/tsconfig.json new file mode 100644 index 000000000..5e5668345 --- /dev/null +++ b/packages/sdk-types/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist" + // No tests expected in a types-only package + ] + // No references needed +} diff --git a/packages/sdk/jest.config.ts b/packages/sdk/jest.config.ts index c815a713a..ff5432614 100644 --- a/packages/sdk/jest.config.ts +++ b/packages/sdk/jest.config.ts @@ -2,6 +2,16 @@ import baseConfig from '../../jest.config.base'; module.exports = { ...baseConfig, + moduleNameMapper: { + '^@metamask/sdk-types$': '/../sdk-types/src/index.ts', + '^@metamask/analytics-client$': '/../analytics-client/src/index.ts', + ...(baseConfig.moduleNameMapper || {}), + }, + transformIgnorePatterns: [ + '/node_modules/(?!(@metamask/analytics-client|@metamask/sdk-types)/)', + '\\\\.pnp\\\\.[^\\\\/]+$', + ], + transform: baseConfig.transform, coveragePathIgnorePatterns: ['./src/types', '/index.ts$'], coverageThreshold: { global: { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f0d9436b8..2e6c9b92c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -49,10 +49,12 @@ }, "dependencies": { "@babel/runtime": "^7.26.0", + "@metamask/analytics-client": "workspace:^", "@metamask/onboarding": "^1.0.1", "@metamask/providers": "16.1.0", "@metamask/sdk-communication-layer": "workspace:*", "@metamask/sdk-install-modal-web": "workspace:*", + "@metamask/sdk-types": "workspace:^", "@paulmillr/qr": "^0.2.1", "bowser": "^2.9.0", "cross-fetch": "^4.0.0", diff --git a/packages/sdk/src/Platform/PlatfformManager.test.ts b/packages/sdk/src/Platform/PlatfformManager.test.ts index 815cd6da0..dbfd1c202 100644 --- a/packages/sdk/src/Platform/PlatfformManager.test.ts +++ b/packages/sdk/src/Platform/PlatfformManager.test.ts @@ -15,6 +15,11 @@ jest.mock('../services/PlatfformManager/openDeeplink'); describe('PlatformManager', () => { let platformManager: PlatformManager; + const originalNavigator = Object.getOwnPropertyDescriptor( + global, + 'navigator', + ); + const originalWindow = Object.getOwnPropertyDescriptor(global, 'window'); const mockGetPlatformType = getPlatformType as jest.MockedFunction< typeof getPlatformType @@ -38,6 +43,23 @@ describe('PlatformManager', () => { }); }); + afterEach(() => { + // Restore original properties after each test + if (originalNavigator) { + Object.defineProperty(global, 'navigator', originalNavigator); + } else { + // @ts-expect-error - Deleting property for cleanup + delete global.navigator; + } + + if (originalWindow) { + Object.defineProperty(global, 'window', originalWindow); + } else { + // @ts-expect-error - Deleting property for cleanup + delete global.window; + } + }); + describe('constructor', () => { it('should initialize correctly with default parameters', () => { platformManager = new PlatformManager({ @@ -98,23 +120,27 @@ describe('PlatformManager', () => { }); it('should return true if the environment is React Native', () => { - global.navigator = { - product: 'ReactNative', - } as any; + Object.defineProperty(global, 'navigator', { + value: { product: 'ReactNative' }, + writable: true, + configurable: true, + }); - global.window = { - navigator: { - product: 'ReactNative', - }, - } as any; + Object.defineProperty(global, 'window', { + value: { navigator: { product: 'ReactNative' } }, + writable: true, + configurable: true, + }); expect(platformManager.isReactNative()).toBe(true); }); it('should return false if the environment is not React Native', () => { - global.navigator = { - product: 'Gecko', - } as any; + Object.defineProperty(global, 'navigator', { + value: { product: 'Gecko' }, + writable: true, + configurable: true, + }); expect(platformManager.isReactNative()).toBe(false); }); @@ -152,29 +178,47 @@ describe('PlatformManager', () => { }); it('should return true if environment is a desktop web browser', () => { - jest - .spyOn(platformManager, 'isNotBrowser') - .mockImplementation() - .mockReturnValue(false); + // Ensure window and navigator exist for Bowser/platform checks if needed by underlying logic + Object.defineProperty(global, 'window', { + value: { navigator: { userAgent: 'Test Desktop Agent' } }, + writable: true, + configurable: true, + }); - jest - .spyOn(platformManager, 'isMobileWeb') - .mockImplementation() - .mockReturnValue(false); + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'Test Desktop Agent', product: 'Gecko' }, // Mock non-RN navigator + writable: true, + configurable: true, + }); + + // Mock dependencies of isDesktopWeb if necessary + // Assuming isDesktopWeb = !isNotBrowser && !isMobileWeb + jest.spyOn(platformManager, 'isNotBrowser').mockReturnValue(false); + jest.spyOn(platformManager, 'isMobileWeb').mockReturnValue(false); + // Also mock isMobile if isDesktopWeb depends on it + jest.spyOn(platformManager, 'isMobile').mockReturnValue(false); expect(platformManager.isDesktopWeb()).toBe(true); }); it('should return false if environment is not a desktop web browser', () => { - jest - .spyOn(platformManager, 'isNotBrowser') - .mockImplementation() - .mockReturnValue(false); + // Ensure window and navigator exist + Object.defineProperty(global, 'window', { + value: { navigator: { userAgent: 'Test Mobile Agent' } }, + writable: true, + configurable: true, + }); - jest - .spyOn(platformManager, 'isMobileWeb') - .mockImplementation() - .mockReturnValue(true); + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'Test Mobile Agent', product: 'Gecko' }, // Mock non-RN navigator + writable: true, + configurable: true, + }); + + jest.spyOn(platformManager, 'isNotBrowser').mockReturnValue(false); // It is a browser + jest.spyOn(platformManager, 'isMobileWeb').mockReturnValue(true); // It is mobile web + // Also mock isMobile if isDesktopWeb depends on it + jest.spyOn(platformManager, 'isMobile').mockReturnValue(true); expect(platformManager.isDesktopWeb()).toBe(false); }); @@ -183,11 +227,18 @@ describe('PlatformManager', () => { describe('isMobile', () => { beforeEach(() => { jest.clearAllMocks(); - global.window = { - navigator: { - userAgent: 'some-user-agent', - }, - } as any; + // Need to define window/navigator here as well for Bowser + Object.defineProperty(global, 'window', { + value: { navigator: { userAgent: 'some-user-agent' } }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'some-user-agent' }, + writable: true, + configurable: true, + }); platformManager = new PlatformManager({ useDeepLink: false, @@ -281,31 +332,76 @@ describe('PlatformManager', () => { }); it('should return false if window is undefined', () => { - global.window = undefined as any; + Object.defineProperty(global, 'window', { + value: undefined, + writable: true, + configurable: true, + }); + expect(platformManager.isMetaMaskMobileWebView()).toBe(false); }); it('should return true if window.ReactNativeWebView exists and userAgent ends with MetaMaskMobile', () => { - global.window = { ReactNativeWebView: {} } as any; - global.navigator = { userAgent: 'someStringMetaMaskMobile' } as any; + Object.defineProperty(global, 'window', { + value: { ReactNativeWebView: {} }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'someStringMetaMaskMobile' }, + writable: true, + configurable: true, + }); + expect(platformManager.isMetaMaskMobileWebView()).toBe(true); }); it('should return false if window.ReactNativeWebView does not exist', () => { - global.window = {} as any; - global.navigator = { userAgent: 'someStringMetaMaskMobile' } as any; + Object.defineProperty(global, 'window', { + value: {}, // Missing ReactNativeWebView + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'someStringMetaMaskMobile' }, + writable: true, + configurable: true, + }); + expect(platformManager.isMetaMaskMobileWebView()).toBe(false); }); it('should return false if userAgent does not end with MetaMaskMobile', () => { - global.window = { ReactNativeWebView: {} } as any; - global.navigator = { userAgent: 'someString' } as any; + Object.defineProperty(global, 'window', { + value: { ReactNativeWebView: {} }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'someString' }, // Doesn't end with MetaMaskMobile + writable: true, + configurable: true, + }); + expect(platformManager.isMetaMaskMobileWebView()).toBe(false); }); it('should return false if neither condition is met', () => { - global.window = {} as any; - global.navigator = { userAgent: 'someString' } as any; + Object.defineProperty(global, 'window', { + value: {}, // Missing ReactNativeWebView + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'navigator', { + value: { userAgent: 'someString' }, // Doesn't end with MetaMaskMobile + writable: true, + configurable: true, + }); + expect(platformManager.isMetaMaskMobileWebView()).toBe(false); }); }); @@ -330,6 +426,7 @@ describe('PlatformManager', () => { platformManager.state = { platformType: PlatformType.DesktopWeb, } as any; + expect(platformManager.isMobileWeb()).toBe(false); }); }); @@ -343,35 +440,73 @@ describe('PlatformManager', () => { }); it('should return true when window is undefined', () => { - global.window = undefined as any; + Object.defineProperty(global, 'window', { + value: undefined, + writable: true, + configurable: true, + }); - expect(platformManager.isNotBrowser()).toBe(true); + expect(PlatformManager.isNotBrowser()).toBe(true); }); it('should return true when window.navigator is undefined', () => { - global.window = {} as any; + Object.defineProperty(global, 'window', { + value: {}, // No navigator property + writable: true, + configurable: true, + }); - expect(platformManager.isNotBrowser()).toBe(true); + expect(PlatformManager.isNotBrowser()).toBe(true); }); it('should return true when global.navigator.product is ReactNative', () => { - global.navigator = { product: 'ReactNative' } as any; + Object.defineProperty(global, 'navigator', { + value: { product: 'ReactNative' }, + writable: true, + configurable: true, + }); + + // Ensure window exists but doesn't interfere + Object.defineProperty(global, 'window', { + value: { navigator: { product: 'Gecko' } }, // Different product on window.navigator + writable: true, + configurable: true, + }); - expect(platformManager.isNotBrowser()).toBe(true); + expect(PlatformManager.isNotBrowser()).toBe(true); }); it('should return true when navigator.product is ReactNative', () => { - global.window = {} as any; - global.window.navigator = { product: 'ReactNative' } as any; + // Ensure global.navigator doesn't interfere + Object.defineProperty(global, 'navigator', { + value: { product: 'Gecko' }, // Different product on global.navigator + writable: true, + configurable: true, + }); - expect(platformManager.isNotBrowser()).toBe(true); + Object.defineProperty(global, 'window', { + value: { navigator: { product: 'ReactNative' } }, + writable: true, + configurable: true, + }); + + expect(PlatformManager.isNotBrowser()).toBe(true); }); it('should return false otherwise', () => { - global.window = { navigator: {} } as any; - global.navigator = {} as any; + Object.defineProperty(global, 'window', { + value: { navigator: { product: 'Gecko' } }, // Regular browser + writable: true, + configurable: true, + }); - expect(platformManager.isNotBrowser()).toBe(false); + Object.defineProperty(global, 'navigator', { + value: { product: 'Gecko' }, // Regular browser + writable: true, + configurable: true, + }); + + expect(PlatformManager.isNotBrowser()).toBe(false); }); }); @@ -406,12 +541,32 @@ describe('PlatformManager', () => { }); }); - it('should return true if isNotBrowser returns false', () => { + // Note: This test now needs to mock the *static* isNotBrowser method + it('should return true if PlatformManager.isNotBrowser returns false', () => { + // Need to ensure navigator/window are defined for the static method to potentially use them + Object.defineProperty(global, 'window', { + value: { navigator: { product: 'Gecko' } }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'navigator', { + value: { product: 'Gecko' }, + writable: true, + configurable: true, + }); jest.spyOn(PlatformManager, 'isNotBrowser').mockReturnValue(false); expect(platformManager.isBrowser()).toBe(true); }); - it('should return false if isNotBrowser returns true', () => { + it('should return false if PlatformManager.isNotBrowser returns true', () => { + // Ensure window/navigator are set up for a non-browser scenario if needed + Object.defineProperty(global, 'window', { + value: undefined, // Example non-browser scenario + writable: true, + configurable: true, + }); + // global.navigator might also need setting depending on isNotBrowser logic jest.spyOn(PlatformManager, 'isNotBrowser').mockReturnValue(true); expect(platformManager.isBrowser()).toBe(false); }); diff --git a/packages/sdk/src/Platform/PlatfformManager.ts b/packages/sdk/src/Platform/PlatfformManager.ts index 73f78430c..1dd2aa442 100644 --- a/packages/sdk/src/Platform/PlatfformManager.ts +++ b/packages/sdk/src/Platform/PlatfformManager.ts @@ -90,13 +90,26 @@ export class PlatformManager { } static isNotBrowser() { - return ( - typeof window === 'undefined' || - !window?.navigator || - (typeof global !== 'undefined' && - global?.navigator?.product === 'ReactNative') || - navigator?.product === 'ReactNative' - ); + // Check if window or window.navigator is missing + if (typeof window === 'undefined' || !window?.navigator) { + return true; + } + + // Check if global.navigator indicates React Native (for environments where window might be mocked but global reflects RN) + if ( + typeof global !== 'undefined' && + global?.navigator?.product === 'ReactNative' + ) { + return true; + } + + // Check if window.navigator indicates React Native + if (window.navigator?.product === 'ReactNative') { + return true; + } + + // Otherwise, it's likely a browser environment + return false; } isNotBrowser() { diff --git a/packages/sdk/src/services/Analytics.test.ts b/packages/sdk/src/services/Analytics.test.ts index dc7873b66..ae12b6391 100644 --- a/packages/sdk/src/services/Analytics.test.ts +++ b/packages/sdk/src/services/Analytics.test.ts @@ -1,24 +1,27 @@ +import { SendAnalytics } from '@metamask/analytics-client'; // Removed unused AnalyticsProps import { - SendAnalytics, + type OriginatorInfo, TrackingEvents, - AnalyticsProps, -} from '@metamask/sdk-communication-layer'; + type TrackingEvent, +} from '@metamask/sdk-types'; // Use types package, import OriginatorInfo import * as loggerModule from '../utils/logger'; import { Analytics } from './Analytics'; // Replace with your actual import path -jest.mock('@metamask/sdk-communication-layer'); +jest.mock('@metamask/analytics-client', () => ({ + SendAnalytics: jest.fn(), +})); const mockSendAnalytics = SendAnalytics as jest.Mock; interface Props { serverUrl: string; - originatorInfo: AnalyticsProps['originatorInfo']; + originatorInfo: OriginatorInfo; // Use OriginatorInfo directly enabled?: boolean; } describe('Analytics', () => { let props: { serverUrl: string; - originatorInfo: AnalyticsProps['originatorInfo']; + originatorInfo: OriginatorInfo; // Use OriginatorInfo directly enabled?: boolean; }; const spyLogger = jest.spyOn(loggerModule, 'logger'); @@ -59,7 +62,7 @@ describe('Analytics', () => { describe('send()', () => { it('should send analytics when enabled', () => { const analytics = new Analytics({ ...props, enabled: true }); - const event: TrackingEvents = TrackingEvents.AUTHORIZED; + const event: TrackingEvent = TrackingEvents.AUTHORIZED; // Use type TrackingEvent analytics.send({ event }); expect(mockSendAnalytics).toHaveBeenCalledWith( expect.objectContaining({ @@ -71,7 +74,7 @@ describe('Analytics', () => { it('should not send analytics when disabled', () => { const analytics = new Analytics({ ...props, enabled: false }); - const event: TrackingEvents = TrackingEvents.AUTHORIZED; + const event: TrackingEvent = TrackingEvents.AUTHORIZED; // Use type TrackingEvent analytics.send({ event }); expect(mockSendAnalytics).not.toHaveBeenCalled(); }); @@ -81,7 +84,7 @@ describe('Analytics', () => { mockSendAnalytics.mockRejectedValue(new Error('Send failed')); const analytics = new Analytics({ ...props }); - const event: TrackingEvents = TrackingEvents.AUTHORIZED; + const event: TrackingEvent = TrackingEvents.AUTHORIZED; // Use type TrackingEvent await analytics.send({ event }); diff --git a/packages/sdk/src/services/Analytics.ts b/packages/sdk/src/services/Analytics.ts index a328458c7..ad4c31a6a 100644 --- a/packages/sdk/src/services/Analytics.ts +++ b/packages/sdk/src/services/Analytics.ts @@ -1,11 +1,15 @@ import { DEFAULT_SERVER_URL, - SendAnalytics, - AnalyticsProps, - TrackingEvents, + // TrackingEvents, // Removed - now from sdk-types } from '@metamask/sdk-communication-layer'; -import { logger } from '../utils/logger'; +import { + SendAnalytics, + type AnalyticsProps, + // type OriginatorInfo, // Removed - now from sdk-types +} from '@metamask/analytics-client'; +import { TrackingEvent, type OriginatorInfo } from '@metamask/sdk-types'; // Import from types package import packageJson from '../../package.json'; +import { logger } from '../utils/logger'; export const ANALYTICS_CONSTANTS = { DEFAULT_ID: 'sdk', @@ -17,7 +21,7 @@ export class Analytics { private enabled: boolean; - private readonly originatorInfo: Readonly; + private readonly originatorInfo: Readonly; constructor({ serverUrl, @@ -25,7 +29,7 @@ export class Analytics { originatorInfo, }: { serverUrl: string; - originatorInfo: AnalyticsProps['originatorInfo']; + originatorInfo: OriginatorInfo; enabled?: boolean; }) { this.serverURL = serverUrl; @@ -37,7 +41,7 @@ export class Analytics { event, params, }: { - event: TrackingEvents; + event: TrackingEvent; params?: Record; }) { if (!this.enabled) { @@ -48,7 +52,7 @@ export class Analytics { id: ANALYTICS_CONSTANTS.DEFAULT_ID, event, sdkVersion: packageJson.version, - ...this.originatorInfo, + originatorInfo: this.originatorInfo, params, }; logger(`[Analytics: send()] event: ${event}`, props); diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts index e3592e9f4..da4d4f4fe 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts @@ -1,4 +1,4 @@ -import { SendAnalytics } from '@metamask/sdk-communication-layer'; +import { SendAnalytics } from '@metamask/analytics-client'; import { STORAGE_PROVIDER_TYPE } from '../../../config'; import { MetaMaskSDK } from '../../../sdk'; import { connectWithExtensionProvider } from '../ProviderManager'; @@ -8,20 +8,18 @@ jest.mock('../ProviderManager', () => ({ connectWithExtensionProvider: jest.fn(), })); -jest.mock('@metamask/sdk-communication-layer', () => ({ - SendAnalytics: jest.fn(), - TrackingEvents: { - SDK_EXTENSION_UTILIZED: 'SDK_EXTENSION_UTILIZED', - }, +jest.mock('@metamask/analytics-client', () => ({ + SendAnalytics: jest.fn().mockResolvedValue(undefined), })); +const mockSendAnalytics = SendAnalytics as jest.Mock; + describe('handleAutoAndExtensionConnections', () => { let instance: MetaMaskSDK; const mockConnect = jest.fn( () => new Promise((resolve) => setTimeout(resolve, 0)), ); const mockIsDesktopWeb = jest.fn(); - const mockSendAnalytics = SendAnalytics as jest.Mock; const mockConnectWithExtensionProvider = connectWithExtensionProvider as jest.Mock; const localStorageMock = { @@ -31,7 +29,7 @@ describe('handleAutoAndExtensionConnections', () => { beforeEach(() => { jest.clearAllMocks(); mockConnectWithExtensionProvider.mockResolvedValue(undefined); - mockSendAnalytics.mockResolvedValue(undefined); + mockSendAnalytics.mockClear(); global.localStorage = localStorageMock as any; diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts index 8a92d2799..4634b270d 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts @@ -1,4 +1,5 @@ import { DEFAULT_SERVER_URL } from '@metamask/sdk-communication-layer'; +import { type OriginatorInfo } from '@metamask/sdk-types'; import { Analytics } from '../../Analytics'; import { MetaMaskSDK } from '../../../sdk'; @@ -17,18 +18,20 @@ export async function setupAnalytics(instance: MetaMaskSDK) { const platformType = instance.platformManager?.getPlatformType(); + const originator: OriginatorInfo = { + url: options.dappMetadata.url ?? '', + title: options.dappMetadata.name ?? '', + dappId: + typeof window === 'undefined' || typeof window.location === 'undefined' + ? options.dappMetadata?.name ?? options.dappMetadata?.url ?? 'N/A' + : window.location.hostname, + platform: platformType ?? '', + source: options._source ?? '', + }; + instance.analytics = new Analytics({ serverUrl: options.communicationServerUrl ?? DEFAULT_SERVER_URL, enabled: options.enableAnalytics, - originatorInfo: { - url: options.dappMetadata.url ?? '', - title: options.dappMetadata.name ?? '', - dappId: - typeof window === 'undefined' || typeof window.location === 'undefined' - ? options.dappMetadata?.name ?? options.dappMetadata?.url ?? 'N/A' - : window.location.hostname, - platform: platformType ?? '', - source: options._source ?? '', - }, + originatorInfo: originator, }); } diff --git a/packages/sdk/src/services/RemoteConnection/ModalManager/showInstallModal.ts b/packages/sdk/src/services/RemoteConnection/ModalManager/showInstallModal.ts index 97001f366..f39c864f8 100644 --- a/packages/sdk/src/services/RemoteConnection/ModalManager/showInstallModal.ts +++ b/packages/sdk/src/services/RemoteConnection/ModalManager/showInstallModal.ts @@ -1,4 +1,4 @@ -import { TrackingEvents } from '@metamask/sdk-communication-layer'; +import { TrackingEvent } from '@metamask/sdk-types'; import { logger } from '../../../utils/logger'; import { RemoteConnectionProps, @@ -39,8 +39,8 @@ export function showInstallModal( event, params, }: { - event: TrackingEvents; - params?: Record; + event: TrackingEvent; + params?: Record; }) => { const extended = { ...params, diff --git a/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts b/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts index 7fea22475..0b19b607f 100644 --- a/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts +++ b/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts @@ -8,8 +8,8 @@ import { KeyInfo, RemoteCommunication, StorageManagerProps, - TrackingEvents, } from '@metamask/sdk-communication-layer'; +import { TrackingEvent } from '@metamask/sdk-types'; import { MetaMaskInstaller } from '../../Platform/MetaMaskInstaller'; import { PlatformManager } from '../../Platform/PlatfformManager'; import { MetaMaskSDK } from '../../sdk'; @@ -67,7 +67,7 @@ export interface RemoteConnectionProps { event, params, }: { - event: TrackingEvents; + event: TrackingEvent; params?: Record; }) => void; }) => { diff --git a/packages/sdk/src/ui/InstallModal/InstallModal-web.ts b/packages/sdk/src/ui/InstallModal/InstallModal-web.ts index fcf99a626..89ae10a6c 100644 --- a/packages/sdk/src/ui/InstallModal/InstallModal-web.ts +++ b/packages/sdk/src/ui/InstallModal/InstallModal-web.ts @@ -1,4 +1,4 @@ -import { TrackingEvents } from '@metamask/sdk-communication-layer'; +import { TrackingEvent } from '@metamask/sdk-types'; import packageJson from '../../../package.json'; import { MetaMaskInstaller } from '../../Platform/MetaMaskInstaller'; import { logger } from '../../utils/logger'; @@ -23,7 +23,7 @@ const sdkWebInstallModal = ({ event, params, }: { - event: TrackingEvents; + event: TrackingEvent; params?: Record; }) => void; }) => { diff --git a/packages/sdk/src/ui/InstallModal/Modal-web.ts b/packages/sdk/src/ui/InstallModal/Modal-web.ts index 3675a4b86..c261c6b72 100644 --- a/packages/sdk/src/ui/InstallModal/Modal-web.ts +++ b/packages/sdk/src/ui/InstallModal/Modal-web.ts @@ -1,4 +1,4 @@ -import { TrackingEvents } from '@metamask/sdk-communication-layer'; +import { TrackingEvent } from '@metamask/sdk-types'; import type { Components } from '@metamask/sdk-install-modal-web'; export interface InstallWidgetProps extends Components.MmInstallModal { @@ -8,7 +8,7 @@ export interface InstallWidgetProps extends Components.MmInstallModal { startDesktopOnboarding: () => void; }; onAnalyticsEvent: (event: { - event: TrackingEvents; + event: TrackingEvent; params?: Record; }) => void; } diff --git a/packages/sdk/src/utils/isOldIOS.test.ts b/packages/sdk/src/utils/isOldIOS.test.ts index 407f445db..b42a0e731 100644 --- a/packages/sdk/src/utils/isOldIOS.test.ts +++ b/packages/sdk/src/utils/isOldIOS.test.ts @@ -1,34 +1,97 @@ import { isOldIOS } from './isOldIOS'; describe('isOldIOS', () => { + const originalNavigator = Object.getOwnPropertyDescriptor( + global, + 'navigator', + ); + const originalWindow = Object.getOwnPropertyDescriptor(global, 'window'); + + afterEach(() => { + // Restore original properties after each test + if (originalNavigator) { + Object.defineProperty(global, 'navigator', originalNavigator); + } else { + // @ts-expect-error - Deleting property for cleanup + delete global.navigator; + } + + if (originalWindow) { + Object.defineProperty(global, 'window', originalWindow); + } else { + // @ts-expect-error - Deleting property for cleanup + delete global.window; + } + }); + it('should return true for iOS versions less than 10', () => { const userAgent = 'CPU iPhone OS 9_3 like Mac OS X'; - global.navigator = { userAgent } as any; - global.window = { MSStream: undefined } as any; + + Object.defineProperty(global, 'navigator', { + value: { userAgent }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'window', { + value: { MSStream: undefined }, + writable: true, + configurable: true, + }); expect(isOldIOS()).toBe(true); }); it('should return false for iOS versions 10 or greater', () => { const userAgent = 'CPU iPhone OS 10_0 like Mac OS X'; - global.navigator = { userAgent } as any; - global.window = { MSStream: undefined } as any; + + Object.defineProperty(global, 'navigator', { + value: { userAgent }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'window', { + value: { MSStream: undefined }, + writable: true, + configurable: true, + }); expect(isOldIOS()).toBe(false); }); it('should return false for non-iOS user agents', () => { const userAgent = 'Android'; - global.navigator = { userAgent } as any; - global.window = { MSStream: undefined } as any; + + Object.defineProperty(global, 'navigator', { + value: { userAgent }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'window', { + value: { MSStream: undefined }, + writable: true, + configurable: true, + }); expect(isOldIOS()).toBe(false); }); it('should return false if MSStream is defined', () => { const userAgent = 'CPU iPhone OS 9_3 like Mac OS X'; - global.navigator = { userAgent } as any; - global.window = { MSStream: {} } as any; + + Object.defineProperty(global, 'navigator', { + value: { userAgent }, + writable: true, + configurable: true, + }); + + Object.defineProperty(global, 'window', { + value: { MSStream: {} }, + writable: true, + configurable: true, + }); expect(isOldIOS()).toBe(false); }); diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 3c28c79d7..02fdcdbfe 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,10 +1,11 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": "./", + "composite": true, "declaration": true, "declarationMap": true, "sourceMap": true, - "strict": true, "outDir": "dist", "target": "es6", "esModuleInterop": true, @@ -16,10 +17,25 @@ "skipLibCheck": true, "types": ["node"], "paths": { - "@metamask/sdk-install-modal-web/*": ["../sdk-install-modal-web/dist/*"] + // Remove specific path mapping here, rely on base and references + // "@metamask/sdk-install-modal-web/*": ["../sdk-install-modal-web/dist/*"] } // "typeRoots": ["packages/sdk/src/types/global.d.ts", "src/types/global.d.ts"] }, - "include": ["./src", "./tools"], - "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] + "include": ["./src", "./tools", "./package.json"], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"], + "references": [ + { + "path": "../sdk-types" + }, + { + "path": "../analytics-client" + }, + { + "path": "../sdk-communication-layer" + }, + { + "path": "../sdk-install-modal-web" + } + ] } diff --git a/packages/sdk/tsconfig.test.json b/packages/sdk/tsconfig.test.json index 2f982970f..8f95ea15a 100644 --- a/packages/sdk/tsconfig.test.json +++ b/packages/sdk/tsconfig.test.json @@ -1,5 +1,7 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { + "baseUrl": ".", "outDir": "dist", "declaration": true, "declarationMap": true, diff --git a/tsconfig.base.json b/tsconfig.base.json index 44d272ff4..4767e6ffd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -3,6 +3,7 @@ "rootDir": ".", "target": "ES6", "moduleResolution": "node", + "strict": true, "esModuleInterop": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, @@ -17,9 +18,11 @@ "@metamask/sdk-communication-layer": [ "packages/sdk-communication-layer/src/index.ts" ], + "@metamask/analytics-client": ["packages/analytics-client/src/index.ts"], + "@metamask/sdk-types": ["packages/sdk-types/src/index.ts"], "@metamask/sdk-install-modal-web": ["packages/sdk-install-modal-web/src/index.ts"], "@metamask/sdk-react": ["packages/sdk-react/src/index.ts"], - "@metamask/sdk-react-native": ["packages/sdk-react-native/src/index.ts"] + // "@metamask/sdk-react-native": ["packages/sdk-react-native/src/index.ts"] } }, "exclude": ["**/node_modules","dist"] diff --git a/yarn.lock b/yarn.lock index f679b9953..b70ca926b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6734,6 +6734,17 @@ __metadata: languageName: node linkType: hard +"@es-joy/jsdoccomment@npm:~0.36.1": + version: 0.36.1 + resolution: "@es-joy/jsdoccomment@npm:0.36.1" + dependencies: + comment-parser: 1.3.1 + esquery: ^1.4.0 + jsdoc-type-pratt-parser: ~3.1.0 + checksum: 28e697779230dc6a95b1f233a8c2a72b64fbea686e407106e5d4292083421a997452731c414de26c10bee86e8e0397c5fb84d6ecfd4b472a29735e1af103ddb6 + languageName: node + linkType: hard + "@es-joy/jsdoccomment@npm:~0.37.0": version: 0.37.1 resolution: "@es-joy/jsdoccomment@npm:0.37.1" @@ -7260,6 +7271,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:8.57.1": + version: 8.57.1 + resolution: "@eslint/js@npm:8.57.1" + checksum: 2afb77454c06e8316793d2e8e79a0154854d35e6782a1217da274ca60b5044d2c69d6091155234ed0551a1e408f86f09dd4ece02752c59568fa403e60611e880 + languageName: node + linkType: hard + "@eslint/js@npm:9.18.0": version: 9.18.0 resolution: "@eslint/js@npm:9.18.0" @@ -9226,6 +9244,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.13.0": + version: 0.13.0 + resolution: "@humanwhocodes/config-array@npm:0.13.0" + dependencies: + "@humanwhocodes/object-schema": ^2.0.3 + debug: ^4.3.1 + minimatch: ^3.0.5 + checksum: eae69ff9134025dd2924f0b430eb324981494be26f0fddd267a33c28711c4db643242cf9fddf7dadb9d16c96b54b2d2c073e60a56477df86e0173149313bd5d6 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.5.0": version: 0.5.0 resolution: "@humanwhocodes/config-array@npm:0.5.0" @@ -9258,7 +9287,7 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.2": +"@humanwhocodes/object-schema@npm:^2.0.2, @humanwhocodes/object-schema@npm:^2.0.3": version: 2.0.3 resolution: "@humanwhocodes/object-schema@npm:2.0.3" checksum: d3b78f6c5831888c6ecc899df0d03bcc25d46f3ad26a11d7ea52944dc36a35ef543fad965322174238d677a43d5c694434f6607532cff7077062513ad7022631 @@ -10654,6 +10683,30 @@ __metadata: languageName: node linkType: hard +"@metamask/analytics-client@workspace:^, @metamask/analytics-client@workspace:packages/analytics-client": + version: 0.0.0-use.local + resolution: "@metamask/analytics-client@workspace:packages/analytics-client" + dependencies: + "@lavamoat/allow-scripts": ^2.3.1 + "@metamask/sdk-types": "workspace:^" + "@types/analytics-node": ^3.1.13 + "@types/body-parser": ^1.19.4 + "@types/cors": ^2.8.15 + "@types/express": ^4.17.20 + "@types/node": ^20.4.1 + "@typescript-eslint/eslint-plugin": ^4.20.0 + "@typescript-eslint/parser": ^4.20.0 + cross-fetch: ^4.0.0 + eslint: ^7.30.0 + eslint-config-prettier: ^8.3.0 + eslint-plugin-prettier: ^3.4.0 + prettier: ^2.8.8 + rimraf: ^6.0.1 + ts-node: ^10.9.1 + typescript: ^4.3.5 + languageName: unknown + linkType: soft + "@metamask/analytics-server@workspace:packages/analytics-server": version: 0.0.0-use.local resolution: "@metamask/analytics-server@workspace:packages/analytics-server" @@ -10837,6 +10890,17 @@ __metadata: languageName: node linkType: hard +"@metamask/eslint-config-nodejs@npm:^12.1.0": + version: 12.1.0 + resolution: "@metamask/eslint-config-nodejs@npm:12.1.0" + peerDependencies: + "@metamask/eslint-config": ^12.0.0 + eslint: ^8.27.0 + eslint-plugin-n: ^15.7.0 + checksum: f4387ba5b0a5173197b960dc59b8e29b7ef02754107b61c3af98f505397048717bd8ba5f9d09555740886c968fc0898520c26899268886f1967bc80f93c26919 + languageName: node + linkType: hard + "@metamask/eslint-config-nodejs@npm:^6.0.0": version: 6.0.0 resolution: "@metamask/eslint-config-nodejs@npm:6.0.0" @@ -10896,6 +10960,21 @@ __metadata: languageName: node linkType: hard +"@metamask/eslint-config@npm:^12.1.0": + version: 12.2.0 + resolution: "@metamask/eslint-config@npm:12.2.0" + peerDependencies: + eslint: ^8.27.0 + eslint-config-prettier: ^8.5.0 + eslint-plugin-import: ~2.26.0 + eslint-plugin-jsdoc: ^39.6.2 || ^41 || ^43.0.7 + eslint-plugin-prettier: ^4.2.1 + eslint-plugin-promise: ^6.1.1 + prettier: ^2.7.1 + checksum: dfd913a712a81db528c662dc2d2d97edf8c34b2053b77c7060f9c117a4f9057d66f2fc87634b5d8860c9ab22c690ad79f40d399bda1e1b9863b0f4d198592a09 + languageName: node + linkType: hard + "@metamask/eslint-config@npm:^6.0.0": version: 6.0.0 resolution: "@metamask/eslint-config@npm:6.0.0" @@ -11197,12 +11276,6 @@ __metadata: languageName: node linkType: hard -"@metamask/sdk-analytics-client@workspace:packages/analytics-client": - version: 0.0.0-use.local - resolution: "@metamask/sdk-analytics-client@workspace:packages/analytics-client" - languageName: unknown - linkType: soft - "@metamask/sdk-communication-layer@npm:0.11.1": version: 0.11.1 resolution: "@metamask/sdk-communication-layer@npm:0.11.1" @@ -11242,10 +11315,12 @@ __metadata: dependencies: "@jest/globals": ^29.3.1 "@lavamoat/allow-scripts": ^2.3.1 + "@metamask/analytics-client": "workspace:^" "@metamask/auto-changelog": 3.1.0 "@metamask/eslint-config": ^6.0.0 "@metamask/eslint-config-nodejs": ^6.0.0 "@metamask/eslint-config-typescript": ^6.0.0 + "@metamask/sdk-types": "workspace:^" "@rollup/plugin-commonjs": ^25.0.0 "@rollup/plugin-json": ^6.0.0 "@rollup/plugin-node-resolve": ^15.0.2 @@ -11259,7 +11334,6 @@ __metadata: "@typescript-eslint/parser": ^4.26.0 bufferutil: ^4.0.8 concurrently: ^9.1.2 - cross-fetch: ^4.0.0 date-fns: ^2.29.3 debug: ^4.3.4 eciesjs: ^0.4.11 @@ -11289,11 +11363,10 @@ __metadata: stream-browserify: ^3.0.0 ts-jest: ^29.0.3 ts-node: ^10.9.1 - typescript: ^5.6.3 + typescript: ^4.3.5 utf-8-validate: ^5.0.2 uuid: ^8.3.2 peerDependencies: - cross-fetch: ^4.0.0 eciesjs: "*" eventemitter2: ^6.4.9 readable-stream: ^3.6.2 @@ -11673,6 +11746,29 @@ __metadata: languageName: unknown linkType: soft +"@metamask/sdk-types@workspace:^, @metamask/sdk-types@workspace:packages/sdk-types": + version: 0.0.0-use.local + resolution: "@metamask/sdk-types@workspace:packages/sdk-types" + dependencies: + "@lavamoat/allow-scripts": ^2.3.1 + "@metamask/eslint-config": ^12.1.0 + "@metamask/eslint-config-nodejs": ^12.1.0 + "@metamask/eslint-config-typescript": ^12.1.0 + "@types/node": ^18.11.18 + "@typescript-eslint/eslint-plugin": ^5.48.1 + "@typescript-eslint/parser": ^5.48.1 + eslint: ^8.31.0 + eslint-config-prettier: ^8.3.0 + eslint-plugin-import: ^2.26.0 + eslint-plugin-jsdoc: ^39.6.4 + eslint-plugin-node: ^11.1.0 + eslint-plugin-prettier: ^4.2.1 + prettier: ^2.8.8 + rimraf: ^3.0.2 + typescript: ^4.3.5 + languageName: unknown + linkType: soft + "@metamask/sdk-ui@workspace:^, @metamask/sdk-ui@workspace:packages/sdk-ui": version: 0.0.0-use.local resolution: "@metamask/sdk-ui@workspace:packages/sdk-ui" @@ -11874,6 +11970,7 @@ __metadata: "@babel/runtime": ^7.26.0 "@jest/globals": ^29.3.1 "@lavamoat/allow-scripts": ^2.3.1 + "@metamask/analytics-client": "workspace:^" "@metamask/auto-changelog": 3.1.0 "@metamask/eslint-config": ^6.0.0 "@metamask/eslint-config-nodejs": ^6.0.0 @@ -11882,6 +11979,7 @@ __metadata: "@metamask/providers": 16.1.0 "@metamask/sdk-communication-layer": "workspace:*" "@metamask/sdk-install-modal-web": "workspace:*" + "@metamask/sdk-types": "workspace:^" "@paulmillr/qr": ^0.2.1 "@react-native-async-storage/async-storage": ^1.19.6 "@rollup/plugin-alias": ^5.1.1 @@ -20145,7 +20243,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.30.5, @typescript-eslint/eslint-plugin@npm:^5.5.0, @typescript-eslint/eslint-plugin@npm:^5.56.0": +"@typescript-eslint/eslint-plugin@npm:^5.30.5, @typescript-eslint/eslint-plugin@npm:^5.48.1, @typescript-eslint/eslint-plugin@npm:^5.5.0, @typescript-eslint/eslint-plugin@npm:^5.56.0": version: 5.62.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" dependencies: @@ -20307,7 +20405,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.30.5, @typescript-eslint/parser@npm:^5.42.0, @typescript-eslint/parser@npm:^5.5.0, @typescript-eslint/parser@npm:^5.56.0": +"@typescript-eslint/parser@npm:^5.30.5, @typescript-eslint/parser@npm:^5.42.0, @typescript-eslint/parser@npm:^5.48.1, @typescript-eslint/parser@npm:^5.5.0, @typescript-eslint/parser@npm:^5.56.0": version: 5.62.0 resolution: "@typescript-eslint/parser@npm:5.62.0" dependencies: @@ -29963,6 +30061,23 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jsdoc@npm:^39.6.4": + version: 39.9.1 + resolution: "eslint-plugin-jsdoc@npm:39.9.1" + dependencies: + "@es-joy/jsdoccomment": ~0.36.1 + comment-parser: 1.3.1 + debug: ^4.3.4 + escape-string-regexp: ^4.0.0 + esquery: ^1.4.0 + semver: ^7.3.8 + spdx-expression-parse: ^3.0.1 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 757444505eabff5bd24ded18fd1a2920031520ba251c84944dd5c12dd2b21460fde6aa6253e454518386c3d7a0fa64f2496e3ba27bd338ec7768cb090ae86cca + languageName: node + linkType: hard + "eslint-plugin-jsdoc@npm:^40.1.1": version: 40.3.0 resolution: "eslint-plugin-jsdoc@npm:40.3.0" @@ -30576,6 +30691,54 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.31.0": + version: 8.57.1 + resolution: "eslint@npm:8.57.1" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.6.1 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.57.1 + "@humanwhocodes/config-array": ^0.13.0 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + "@ungap/structured-clone": ^1.2.0 + ajv: ^6.12.4 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.2.2 + eslint-visitor-keys: ^3.4.3 + espree: ^9.6.1 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + graphemer: ^1.4.0 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + strip-ansi: ^6.0.1 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: e2489bb7f86dd2011967759a09164e65744ef7688c310bc990612fc26953f34cc391872807486b15c06833bdff737726a23e9b4cdba5de144c311377dc41d91b + languageName: node + linkType: hard + "eslint@npm:^8.48.0": version: 8.48.0 resolution: "eslint@npm:8.48.0" @@ -33227,6 +33390,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^11.0.0": + version: 11.0.1 + resolution: "glob@npm:11.0.1" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^4.0.1 + minimatch: ^10.0.0 + minipass: ^7.1.2 + package-json-from-dist: ^1.0.0 + path-scurry: ^2.0.0 + bin: + glob: dist/esm/bin.mjs + checksum: ffbbafe1d2dae2fa68f190ac76df7254e840b27f59df34129fd658bd9da0c50b538d144eb0962dc7fa71cdaccf3fe108f045d4a15b3f5815e465749a6bf00965 + languageName: node + linkType: hard + "glob@npm:^6.0.1": version: 6.0.4 resolution: "glob@npm:6.0.4" @@ -35724,6 +35903,15 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^4.0.1": + version: 4.1.0 + resolution: "jackspeak@npm:4.1.0" + dependencies: + "@isaacs/cliui": ^8.0.2 + checksum: cfc2b527e4b51e55a21961c091b37dfd177341196d089a355eb692f8895b24b6caac912b4f594b64cfaf6d2bdf085898361379b74f27936ed3d5e0ba75f96e94 + languageName: node + linkType: hard + "jake@npm:^10.8.5": version: 10.8.7 resolution: "jake@npm:10.8.7" @@ -37962,6 +38150,13 @@ __metadata: languageName: node linkType: hard +"jsdoc-type-pratt-parser@npm:~3.1.0": + version: 3.1.0 + resolution: "jsdoc-type-pratt-parser@npm:3.1.0" + checksum: 2f437b57621f1e481918165f6cf0e48256628a9e510d8b3f88a2ab667bf2128bf8b94c628b57c43e78f555ca61983e9c282814703840dc091d2623992214a061 + languageName: node + linkType: hard + "jsdoc-type-pratt-parser@npm:~4.0.0": version: 4.0.0 resolution: "jsdoc-type-pratt-parser@npm:4.0.0" @@ -39090,6 +39285,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.0.0": + version: 11.1.0 + resolution: "lru-cache@npm:11.1.0" + checksum: 6274e90b5fdff87570fe26fe971467a5ae1f25f132bebe187e71c5627c7cd2abb94b47addd0ecdad034107667726ebde1abcef083d80f2126e83476b2c4e7c82 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -40888,6 +41090,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.0.0": + version: 10.0.1 + resolution: "minimatch@npm:10.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: f5b63c2f30606091a057c5f679b067f84a2cd0ffbd2dbc9143bda850afd353c7be81949ff11ae0c86988f07390eeca64efd7143ee05a0dab37f6c6b38a2ebb6c + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -41041,7 +41252,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^7.0.3": +"minipass@npm:^7.0.3, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 2bfd325b95c555f2b4d2814d49325691c7bee937d753814861b0b49d5edcda55cbbf22b6b6a60bb91eddac8668771f03c5ff647dcd9d0f798e9548b9cdc46ee3 @@ -42701,6 +42912,13 @@ __metadata: languageName: node linkType: hard +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 + languageName: node + linkType: hard + "pako@npm:~0.2.0": version: 0.2.9 resolution: "pako@npm:0.2.9" @@ -42972,6 +43190,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^2.0.0": + version: 2.0.0 + resolution: "path-scurry@npm:2.0.0" + dependencies: + lru-cache: ^11.0.0 + minipass: ^7.1.2 + checksum: 9953ce3857f7e0796b187a7066eede63864b7e1dfc14bf0484249801a5ab9afb90d9a58fc533ebb1b552d23767df8aa6a2c6c62caf3f8a65f6ce336a97bbb484 + languageName: node + linkType: hard + "path-strip-sep@npm:^1.0.17": version: 1.0.17 resolution: "path-strip-sep@npm:1.0.17" @@ -47045,6 +47273,18 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^6.0.1": + version: 6.0.1 + resolution: "rimraf@npm:6.0.1" + dependencies: + glob: ^11.0.0 + package-json-from-dist: ^1.0.0 + bin: + rimraf: dist/esm/bin.mjs + checksum: 8ba5b84131c1344e9417cb7e8c05d8368bb73cbe5dd4c1d5eb49fc0b558209781658d18c450460e30607d0b7865bb067482839a2f343b186b07ae87715837e66 + languageName: node + linkType: hard + "rimraf@npm:~2.4.0": version: 2.4.5 resolution: "rimraf@npm:2.4.5"