Files
trezor-suite/scripts/ci/check-connect-data.ts
2025-04-22 10:00:30 +02:00

237 lines
9.4 KiB
TypeScript

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<DeviceModel, { authenticity: string; authenticityDev: string }>,
);
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();