import fetch from 'cross-fetch'; import fs from 'fs-extra'; import path from 'path'; import crypto from 'crypto'; import { comment, commit } from './helpers'; const { exec } = require('./helpers'); const AUTHENTICITY_BASE_URL = 'https://data.trezor.io'; const ROOT = path.join(__dirname, '..', '..'); const CONFIG_FILE_PATH = path.join(ROOT, 'packages/connect/src/data/deviceAuthenticityConfig.ts'); const DEVICES_SUPPORTING_DEVICE_AUTHENTICITY_CHECK = ['T2B1', 'T3B1', 'T3T1', 'T3W1'] as const; type DeviceModel = (typeof DEVICES_SUPPORTING_DEVICE_AUTHENTICITY_CHECK)[number]; const authenticityPaths = DEVICES_SUPPORTING_DEVICE_AUTHENTICITY_CHECK.reduce( (acc, device) => { acc[device] = { authenticity: `firmware/${device.toLowerCase()}/authenticity.json`, authenticityDev: `firmware/${device.toLowerCase()}/authenticity-dev.json`, }; return acc; }, {} as Record, ); const fetchJSON = async (url: string) => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.statusText}`); } return response.json(); }; const getLatestTimestamp = (timestamps: string[]): string | null => { if (timestamps.length === 0) { return null; } const latestTime = Math.max(...timestamps.map(Date.parse)); return new Date(latestTime).toISOString(); }; const emptyAuthenticityConfig = { root_pubkeys: [], ca_pubkeys: [], timestamp: 0, }; const updateConfigFromJSON = async () => { try { const devicesKeys = Object.keys(authenticityPaths) as DeviceModel[]; // Import the current configuration object let { deviceAuthenticityConfig } = require(CONFIG_FILE_PATH); const timestamps: string[] = []; for (const deviceKey of devicesKeys) { const { authenticity, authenticityDev } = authenticityPaths[deviceKey]; const authenticityUrl = `${AUTHENTICITY_BASE_URL}/${authenticity}`; // TODO: for now we do not have production keys for `T3W1` so we include it empty. const authenticityData = deviceKey === 'T3W1' ? emptyAuthenticityConfig : await fetchJSON(authenticityUrl); timestamps.push(authenticityData.timestamp); const authenticityDevUrl = `${AUTHENTICITY_BASE_URL}/${authenticityDev}`; const authenticityDevData = await fetchJSON(authenticityDevUrl); deviceAuthenticityConfig[deviceKey] = { rootPubKeys: authenticityData.root_pubkeys, caPubKeys: authenticityData.ca_pubkeys, debug: { rootPubKeys: authenticityDevData.root_pubkeys, caPubKeys: authenticityDevData.ca_pubkeys, }, }; } const latestTimestamp = getLatestTimestamp(timestamps); if (!latestTimestamp) { // Sanity check and type safety. throw new Error('Timestamp should always be present.'); } deviceAuthenticityConfig['timestamp'] = latestTimestamp; const updatedConfigString = ` /** THIS FILE IS AUTOMATICALLY UPDATED by script ci/scripts/check-connect-data.ts */ import { DeviceAuthenticityConfig } from './deviceAuthenticityConfigTypes'; /** * How to update this config or check Sentry "Device authenticity invalid!" error? Please read this internal description: * https://www.notion.so/satoshilabs/Device-authenticity-check-b8656a0fe3ab4a0d84c61534a73de462?pvs=4 */ export const deviceAuthenticityConfig: DeviceAuthenticityConfig = ${JSON.stringify(deviceAuthenticityConfig, null, 4)}; `; await fs.writeFile(CONFIG_FILE_PATH, updatedConfigString); await exec('yarn', ['prettier', '--write', CONFIG_FILE_PATH]); console.log('Configuration updated successfully.'); console.log('Checking if there were changes.'); const changes = await exec('git', ['diff', CONFIG_FILE_PATH]); // Use the content to generate the hash in the branch so it is the same with same content. // If we would use the hash provided by Git it would be different because it contains date as well. const fileContent = await fs.readFile(CONFIG_FILE_PATH, 'utf8'); const hash = crypto.createHash('sha256').update(fileContent).digest('hex'); // Use the hash to create branch name to avoid using a branch that already exists. const branchName = `chore/update-device-authenticity-config-${hash}`; // Check if PR with same hash, so no new changes is already open, if so, ignore it, no new changes to add. const prAlreadyOpen = await exec('gh', [ 'search', 'prs', '--repo=trezor/trezor-suite', `--head=${branchName}`, '--state=open', ]); console.log('prAlreadyOpen', prAlreadyOpen); const isSamePrAlreadyOpen = prAlreadyOpen.stdout !== ''; const isChangesFromRemote = changes.stdout !== ''; if (isSamePrAlreadyOpen) { console.log('There is already a PR open with same changes.'); } if (!isSamePrAlreadyOpen && isChangesFromRemote) { console.log('There were changes in keys.'); // Before creating the new PR with new keys we check if there was already previous one. // We can delete the previous one since latest one will contain all new changes. // If you need to update search query you can test in GH: https://github.com/search and // use the documentation https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests const prList = await exec('gh', [ 'search', 'prs', '--repo=trezor/trezor-suite', '--head=chore/update-device-authenticity-config', '--state=open', ]); console.log('prList', prList); if (prList.stdout !== '') { const prNumbers = prList.stdout.match(/(?<=\t)(\d+)(?=\t)/g); console.log(`Found open pull requests ${prNumbers}. Closing...`); for (const prNumber of prNumbers) { try { console.log(`Commenting on PR ${prNumber}`); comment({ prNumber, body: `Closing this PR since we are going to create new one with latest changes in ${AUTHENTICITY_BASE_URL}`, }); console.log(`Closing PR ${prNumber}`); await exec('gh', [ 'pr', 'close', prNumber, '--repo', 'trezor/trezor-suite', ]); console.log(`Closed PR #${prNumber}`); } catch (error) { console.error(`Failed to close PR #${prNumber}:`, error.message); } } try { console.log(`Deleting branch ${branchName}`); await exec('gh', ['branch', '-d', branchName, '--repo', 'trezor/trezor-suite']); console.log(`Deleted branch ${branchName}`); } catch (error) { console.error(`Failed to delete branch ${branchName}:`, error.message``); } } else { console.log(`No open pull requests found.`); } const commitMessage = 'chore(connect): update device authenticity config'; await exec('git', ['checkout', '-b', branchName]); await commit({ path: ROOT, message: commitMessage, }); // If the branch was already created this will fail, and this is desired feature because we // do not want to create 2 branches and PRs with same content. await exec('git', ['push', 'origin', branchName]); const pr = await exec('gh', [ 'pr', 'create', '--repo', 'trezor/trezor-suite', '--title', `${commitMessage}`, '--body-file', 'docs/packages/connect/check-connect-data.md', '--base', 'develop', '--head', branchName, ]); console.log('pr', pr); const prUrlMatch = pr.stdout.match(/(https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+)/); console.log('prUrlMatch', prUrlMatch); if (!prUrlMatch) { throw new Error('Failed to extract PR URL'); } const prUrl = prUrlMatch[0]; // We get the PR number from the PR URL provided by the `gh pr create ...`. const prNumber = prUrl.split('/').pop(); console.log(`Created PR: ${prUrl}`); // Adding label to the created PR. await exec('gh', ['pr', 'edit', `${prNumber}`, '--add-label', 'no-project']); console.log(`Added label to PR #${prNumber}`); } } catch (error) { console.error(`Error updating configuration: ${error.message}`); throw new Error(error.message); } }; updateConfigFromJSON();