diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 81198ce1..2367a17e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,2 @@ -* @VictorVicente @polymath-eric @prashantasdeveloper @sansan -Jenkinsfile @PolymeshAssociation/SRE -./github @PolymeshAssociation/SRE +* @PolymeshAssociation/middleware +/.github/ @PolymeshAssociation/SRE diff --git a/.github/workflows/fast-forward.yml b/.github/workflows/fast-forward.yml index 6c7eddee..627d145b 100644 --- a/.github/workflows/fast-forward.yml +++ b/.github/workflows/fast-forward.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Fast forwarding - uses: sequoia-pgp/fast-forward@v1 + uses: sequoia-pgp/fast-forward@ea7628bedcb0b0b96e94383ada458d812fca4979 # v1.0.0 with: merge: true # To reduce the workflow's verbosity, use 'on-error' diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 7156aec6..7619534f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -17,6 +17,9 @@ jobs: cache: 'yarn' cache-dependency-path: 'yarn.lock' - name: install dependencies - run: yarn --immutable + # dependency lifecycle scripts are disabled to avoid executing untrusted code + run: yarn --immutable --mode=skip-build + - name: run trusted postinstall + run: yarn run postinstall - name: test run: yarn test:integration diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af0dbee1..5a481cd8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,10 @@ jobs: node-version: '22.x' cache: 'yarn' - name: install dependencies - run: yarn --immutable + # dependency lifecycle scripts are disabled to avoid executing untrusted code + run: yarn --immutable --mode=skip-build + - name: run trusted postinstall + run: yarn run postinstall - name: lint run: yarn lint @@ -50,7 +53,10 @@ jobs: node-version: '22.x' cache: 'yarn' - name: install dependencies - run: yarn --immutable + # dependency lifecycle scripts are disabled to avoid executing untrusted code + run: yarn --immutable --mode=skip-build + - name: run trusted postinstall + run: yarn run postinstall - name: build run: yarn build @@ -70,7 +76,10 @@ jobs: node-version: '22.x' cache: 'yarn' - name: install dependencies - run: yarn --immutable + # dependency lifecycle scripts are disabled to avoid executing untrusted code + run: yarn --immutable --mode=skip-build + - name: run trusted postinstall + run: yarn run postinstall - name: test run: yarn test:cov @@ -91,7 +100,10 @@ jobs: node-version: '22.x' cache: 'yarn' - name: install dependencies - run: yarn --immutable + # dependency lifecycle scripts are disabled to avoid executing untrusted code + run: yarn --immutable --mode=skip-build + - name: run trusted postinstall + run: yarn run postinstall # - name: integration test # run: yarn test:integration - name: Setup SSH signing key @@ -138,7 +150,7 @@ jobs: steps: - name: Checking if fast forwarding is possible - uses: sequoia-pgp/fast-forward@v1 + uses: sequoia-pgp/fast-forward@ea7628bedcb0b0b96e94383ada458d812fca4979 # v1.0.0 with: merge: false # To reduce the workflow's verbosity, use 'on-error' diff --git a/Dockerfile b/Dockerfile index eba4b93d..eb075e54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ FROM node:lts-alpine3.17 AS builder RUN apk add --no-cache \ - python3 \ - make \ cmake \ g++ \ - jq + jq \ + make \ + python3 WORKDIR /app/builder RUN chown -R node: /app diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 0f08c36e..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,93 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////// - -def withSecretEnv(List varAndPasswordList, Closure closure) { - wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: varAndPasswordList]) { - withEnv(varAndPasswordList.collect { "${it.var}=${it.password}" }) { - closure() - } - } -} - -////////////////////////////////////////////////////////////////////////////////////////// - -node { - - env.PROJECT_NAME = 'polymesh-rest-api' - env.GIT_REPO = "ssh://git@ssh.gitea.polymesh.dev:4444/Deployment/${PROJECT_NAME}.git" - - properties([[$class: 'BuildDiscarderProperty', - strategy: [$class: 'LogRotator', - numToKeepStr: '7', - daysToKeepStr: '7', - artifactNumToKeepStr: '7', - artifactDaysToKeepStr: '7']]]) - - if (env.CHANGE_BRANCH) { - env.GIT_BRANCH = env.CHANGE_BRANCH // Job was started from a pull request - } else { - env.GIT_BRANCH = env.BRANCH_NAME - } - - dir("${JENKINS_HOME}/workspace/${JOB_NAME}/${BUILD_NUMBER}") { - - withCredentials([ - usernamePassword(credentialsId: 'vault_approle', - usernameVariable: 'VAULT_ROLE_ID', - passwordVariable: 'VAULT_SECRET_ID'), - ]) { - - env.VAULT_ADDR = 'https://127.0.0.1:8200/' - env.VAULT_CACERT = '/etc/ssl/certs/vault.tls.chain.pem' - - withSecretEnv([[var: 'VAULT_TOKEN', - password: sh (returnStdout: true, - label: 'Login To Vault', - script: '''\ - vault write -field=token \ - -format=table \ - auth/approle/login \ - role_id="$VAULT_ROLE_ID" \ - secret_id="$VAULT_SECRET_ID" - '''.stripIndent()).trim()]]) { - stage('Clone Repo') { - sh (label: 'Clone Repo', - script: '/usr/local/bin/gitea-clone-repo.sh') - } - } - } - - dir("${PROJECT_NAME}") { - - env.AWS_ACCOUNT_NUMBER = env.AWS_ACCOUNT_NUMBER_DATACENTER_PRIMARY - env.AWS_REGION = env.AWS_REGION_DATACENTER_PRIMARY - env.AWS_DEFAULT_REGION = env.AWS_REGION - env.CONTAINER_REGISTRY = "${env.AWS_ACCOUNT_NUMBER}.dkr.ecr.${AWS_REGION}.amazonaws.com" - env.GIT_COMMIT = sh (returnStdout: true, - label: 'Read Git Commit', - script: 'git rev-parse HEAD').trim() - - echo "GIT_COMMIT: ${env.GIT_COMMIT}" - - stage('Build') { - sh (label: 'Build', - script: '''#!/bin/bash - docker build -f Dockerfile -t "${CONTAINER_REGISTRY}/polymesh/${PROJECT_NAME}:${GIT_COMMIT}" . - ''') - } - - stage('Push') { - sh (label: 'Push', - script: '''#!/bin/bash - - aws ecr get-login-password | \ - docker login "$CONTAINER_REGISTRY" --username AWS --password-stdin - - docker push "${CONTAINER_REGISTRY}/polymesh/${PROJECT_NAME}:${GIT_COMMIT}" || true - ''') - } - - } - } -} - -////////////////////////////////////////////////////////////////////////////////////////// diff --git a/jest.config.js b/jest.config.js index 66deaacd..407e2490 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,13 +1,13 @@ module.exports = { preset: 'ts-jest/presets/js-with-babel', - transformIgnorePatterns: ['/node_modules/(?![@polymeshassociation/src]).+\\.js$'], + transformIgnorePatterns: [String.raw`/node_modules/(?![@polymeshassociation/src]).+\.js$`], testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], testPathIgnorePatterns: ['/dist/*'], moduleNameMapper: { '~/(.*)': '/src/$1', }, - testRegex: '.*\\.spec\\.ts$', + testRegex: String.raw`.*\.spec\.ts$`, coverageDirectory: './coverage', collectCoverage: true, restoreMocks: true, diff --git a/prepareRelease.sh b/prepareRelease.sh index d5b78aad..cee6ec91 100755 --- a/prepareRelease.sh +++ b/prepareRelease.sh @@ -10,7 +10,7 @@ CHAIN_TAG='8.0.0-testnet-debian' # This lets it work on arm64, like Mac Books ARCHITECTURE=$(uname -m) CHAIN_REPO=polymeshassociation/polymesh -if [ "$ARCHITECTURE" = "arm64" ]; then +if [[ "$ARCHITECTURE" == "arm64" ]]; then CHAIN_REPO="polymeshassociation/polymesh-arm64" fi diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh index ae41a128..f00ee6bf 100755 --- a/scripts/integration-test.sh +++ b/scripts/integration-test.sh @@ -9,7 +9,8 @@ git clone https://github.com/PolymeshAssociation/polymesh-dev-env.git "$DEV_ENV_ pushd "$INTEGRATION_DIR" -yarn +# lifecycle scripts are disabled to avoid executing untrusted dependency code +yarn --mode=skip-build echo "starting up environment" yarn test:start echo "env is set up" @@ -24,14 +25,16 @@ export POLYMESH_MIDDLEWARE_V2_URL="http://localhost:3000" # register cleanup to stop the background service function cleanup() { echo "cleaning up test environment" - if [ -n "${SERVICE_PID}" ]; then + if [[ -n "${SERVICE_PID}" ]]; then echo "Cleaning up service with PID: ${SERVICE_PID}" kill "${SERVICE_PID}" fi yarn test:stop - rm -rf $DEV_ENV_DIR + rm -rf "$DEV_ENV_DIR" + + return 0 } trap cleanup EXIT INT TERM diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..b2941a93 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,10 @@ +sonar.projectKey=PolymeshAssociation_polymesh-rest-api +sonar.organization=polymeshassociation + +# The datastore repo specs delegate to a shared, reusable suite that each implementation +# must pass (e.g. `NotificationRepo.test(repo)`). The `it(...)` blocks live in the +# `*.repo.suite.ts` helpers, so Sonar's static analysis cannot see them and wrongly reports +# S2187 ("Add some tests to this file or delete it"). These files do run real tests. +sonar.issue.ignore.multicriteria=e1 +sonar.issue.ignore.multicriteria.e1.ruleKey=typescript:S2187 +sonar.issue.ignore.multicriteria.e1.resourceKey=src/datastore/**/repos/*.repo.spec.ts diff --git a/src/claims/decorators/get-custom-claim-type.pipe.ts b/src/claims/decorators/get-custom-claim-type.pipe.ts index c4908423..31f49b87 100644 --- a/src/claims/decorators/get-custom-claim-type.pipe.ts +++ b/src/claims/decorators/get-custom-claim-type.pipe.ts @@ -17,5 +17,5 @@ export class GetCustomClaimTypePipe implements PipeTransform { } function isNumericString(value: string): boolean { - return !isNaN(Number(value)); + return !Number.isNaN(Number(value)); } diff --git a/src/commands/write-swagger.ts b/src/commands/write-swagger.ts index 00d4faba..26464121 100644 --- a/src/commands/write-swagger.ts +++ b/src/commands/write-swagger.ts @@ -1,6 +1,7 @@ +import { writeFileSync } from 'node:fs'; + import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { writeFileSync } from 'fs'; import { AppModule } from '~/app.module'; import { swaggerDescription, swaggerTitle } from '~/common/utils'; diff --git a/src/common/decorators/swagger.ts b/src/common/decorators/swagger.ts index 900dedae..5ee88b0a 100644 --- a/src/common/decorators/swagger.ts +++ b/src/common/decorators/swagger.ts @@ -23,18 +23,16 @@ import { ResultsModel } from '~/common/models/results.model'; export const ApiArrayResponse = ( model: TModel, { - paginated, + paginated = true, example, examples, description, }: { - paginated: boolean; + paginated?: boolean; example?: unknown; examples?: unknown[] | Record; description?: string; - } = { - paginated: true, - } + } = {} ): ReturnType => { const extraModels = []; let items; diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts index 845c2684..996ea18f 100644 --- a/src/common/utils/functions.ts +++ b/src/common/utils/functions.ts @@ -6,7 +6,6 @@ import { TxTags, } from '@polymeshassociation/polymesh-sdk/types'; import { randomBytes } from 'crypto'; -import { flatten } from 'lodash'; import { promisify } from 'util'; import { TransactionBaseDto } from '~/common/dto/transaction-base-dto'; @@ -28,7 +27,7 @@ import { import { ResultType } from '~/transactions/types'; export function getTxTags(): string[] { - return flatten(Object.values(TxTags).map(txTag => Object.values(txTag))); + return Object.values(TxTags).flatMap(txTag => Object.values(txTag)); } export function getTxTagsWithModuleNames(): string[] { diff --git a/src/corporate-actions/decorators/validation.ts b/src/corporate-actions/decorators/validation.ts index c11b4f8a..890f3981 100644 --- a/src/corporate-actions/decorators/validation.ts +++ b/src/corporate-actions/decorators/validation.ts @@ -15,7 +15,7 @@ export function IsCaCheckpoint() { validator: { async validate(value: unknown) { if (value instanceof Date) { - return !isNaN(new Date(value).getTime()); + return !Number.isNaN(new Date(value).getTime()); } if (value instanceof CorporateActionCheckpointDto) { return (await validateClass(value)).length === 0; diff --git a/src/datastore/postgres/repos/api-keys.repo.ts b/src/datastore/postgres/repos/api-keys.repo.ts index 32f58c00..bd4a714a 100644 --- a/src/datastore/postgres/repos/api-keys.repo.ts +++ b/src/datastore/postgres/repos/api-keys.repo.ts @@ -11,7 +11,7 @@ import { UserModel } from '~/users/model/user.model'; @Injectable() export class PostgresApiKeyRepo implements ApiKeyRepo { - constructor(@InjectRepository(ApiKey) private apiKeyRepo: Repository) {} + constructor(@InjectRepository(ApiKey) private readonly apiKeyRepo: Repository) {} public async createApiKey(user: UserModel): Promise { const secret = await generateBase64Secret(32); diff --git a/src/datastore/postgres/repos/notification.repo.ts b/src/datastore/postgres/repos/notification.repo.ts index f8cd326e..c02302c2 100644 --- a/src/datastore/postgres/repos/notification.repo.ts +++ b/src/datastore/postgres/repos/notification.repo.ts @@ -11,7 +11,10 @@ import { NotificationParams } from '~/notifications/types'; @Injectable() export class PostgresNotificationRepo implements NotificationRepo { - constructor(@InjectRepository(Notification) private notificationRepo: Repository) {} + constructor( + @InjectRepository(Notification) + private readonly notificationRepo: Repository + ) {} public async create(params: NotificationParams): Promise { const entity = this.notificationRepo.create({ diff --git a/src/datastore/postgres/repos/subscription.repo.ts b/src/datastore/postgres/repos/subscription.repo.ts index 756a243d..0b1dcc48 100644 --- a/src/datastore/postgres/repos/subscription.repo.ts +++ b/src/datastore/postgres/repos/subscription.repo.ts @@ -11,7 +11,10 @@ import { SubscriptionParams } from '~/subscriptions/types'; @Injectable() export class PostgresSubscriptionRepo implements SubscriptionRepo { - constructor(@InjectRepository(Subscription) private subscriptionRepo: Repository) {} + constructor( + @InjectRepository(Subscription) + private readonly subscriptionRepo: Repository + ) {} public async create(params: SubscriptionParams): Promise { const entity = this.subscriptionRepo.create({ diff --git a/src/datastore/postgres/source.ts b/src/datastore/postgres/source.ts index e75d9905..bfc27890 100644 --- a/src/datastore/postgres/source.ts +++ b/src/datastore/postgres/source.ts @@ -1,7 +1,8 @@ /* istanbul ignore file */ +import path from 'node:path'; + import * as dotenv from 'dotenv'; -import path from 'path'; import { DataSource } from 'typeorm'; import { readPostgresConfigFromEnv } from '~/datastore/postgres/utils'; diff --git a/src/identities/dto/register-identity.dto.ts b/src/identities/dto/register-identity.dto.ts index e9414730..4baf89fd 100644 --- a/src/identities/dto/register-identity.dto.ts +++ b/src/identities/dto/register-identity.dto.ts @@ -39,7 +39,7 @@ export class RegisterIdentityDto extends TransactionBaseDto { @ApiPropertyOptional({ description: 'Date at which the Identity will expire. Deprecated: as of chain v8 this no longer has any on-chain effect', - example: new Date(new Date().getTime() + +365 * 24 * 60 * 60 * 1000).toISOString(), + example: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), type: 'string', deprecated: true, }) diff --git a/src/message/artemis/artemis.service.ts b/src/message/artemis/artemis.service.ts index 54f6d33c..9cf8d525 100644 --- a/src/message/artemis/artemis.service.ts +++ b/src/message/artemis/artemis.service.ts @@ -30,8 +30,8 @@ type AddressStore = Record; @Injectable() export class ArtemisService extends MessageService implements OnApplicationShutdown { - private receivers: Receiver[] = []; - private addressStore: Partial = {}; + private readonly receivers: Receiver[] = []; + private readonly addressStore: Partial = {}; private connectionPromise?: Promise; constructor( diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 999cbc31..fd1384c2 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -19,8 +19,8 @@ import { SubscriptionsService } from '~/subscriptions/subscriptions.service'; @Injectable() export class NotificationsService { - private maxTries: number; - private retryInterval: number; + private readonly maxTries: number; + private readonly retryInterval: number; constructor( @Inject(notificationsConfig.KEY) config: ConfigType, diff --git a/src/notifications/notifications.util.ts b/src/notifications/notifications.util.ts index 13decd1f..d6e9b5df 100644 --- a/src/notifications/notifications.util.ts +++ b/src/notifications/notifications.util.ts @@ -1,4 +1,5 @@ -import { createHmac } from 'crypto'; +import { createHmac } from 'node:crypto'; + import stringify from 'json-stable-stringify'; import { NotificationPayload } from '~/notifications/types'; diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts index 6057c027..a46972cd 100644 --- a/src/offline-starter/offline-starter.service.ts +++ b/src/offline-starter/offline-starter.service.ts @@ -1,6 +1,7 @@ +import { randomUUID } from 'node:crypto'; + import { Injectable } from '@nestjs/common'; import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; -import { randomUUID } from 'crypto'; import { AddressName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 7e5329b7..d5c26f24 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -34,7 +34,7 @@ export class OfflineSubmitterService { public async submit(body: OfflineSignatureModel): Promise { const { id, signature, payload } = body; const { address, nonce: rawNonce } = payload.payload; - const nonce = parseInt(rawNonce, 16); + const nonce = Number.parseInt(rawNonce, 16); this.logger.debug(`received signature for: ${id}`); const transaction = await this.offlineTxRepo.createTx({ diff --git a/src/polymesh/polymesh.service.ts b/src/polymesh/polymesh.service.ts index 80b204b6..945cef00 100644 --- a/src/polymesh/polymesh.service.ts +++ b/src/polymesh/polymesh.service.ts @@ -9,7 +9,7 @@ import { ScheduleService } from '~/schedule/schedule.service'; @Injectable() export class PolymeshService implements OnModuleDestroy, OnModuleInit { - private heartbeatIntervalId = 'polymeshHeartbeat'; + private readonly heartbeatIntervalId = 'polymeshHeartbeat'; constructor( @Inject(POLYMESH_API) public readonly polymeshApi: Polymesh, diff --git a/src/signing/config/signers.config.ts b/src/signing/config/signers.config.ts index 5aec94e9..204bebda 100644 --- a/src/signing/config/signers.config.ts +++ b/src/signing/config/signers.config.ts @@ -1,7 +1,8 @@ /* istanbul ignore file */ +import { readFileSync } from 'node:fs'; + import { registerAs } from '@nestjs/config'; -import { readFileSync } from 'fs'; export default registerAs('signer-accounts', () => { const { diff --git a/src/signing/services/fireblocks-signing.service.ts b/src/signing/services/fireblocks-signing.service.ts index 3facf420..ff020cf6 100644 --- a/src/signing/services/fireblocks-signing.service.ts +++ b/src/signing/services/fireblocks-signing.service.ts @@ -27,7 +27,7 @@ export class FireblocksSigningService extends SigningService { private handleToDerivationPath(handle: string): DerivationPath { const sections = handle.split('-').map(Number); - if (sections.some(isNaN) || sections.length > 3 || handle === '') { + if (sections.some(Number.isNaN) || sections.length > 3 || handle === '') { throw new AppValidationError( 'Fireblocks `signer` field should be 3 integers formatted like: `x-y-z`' ); diff --git a/src/subscriptions/subscriptions.service.spec.ts b/src/subscriptions/subscriptions.service.spec.ts index 6d76a656..4de36b83 100644 --- a/src/subscriptions/subscriptions.service.spec.ts +++ b/src/subscriptions/subscriptions.service.spec.ts @@ -292,12 +292,10 @@ describe('SubscriptionsService', () => { }); describe('batchBumpNonce', () => { - it('should mark a group of subscriptions as done', async () => { + it('should bump the nonce of a group of subscriptions', async () => { await service.batchBumpNonce([1, 2]); - const result = await service.findAll(); - - expect(result.every(({ nextNonce }) => nextNonce === 1)); + expect(mockSubscriptionRepo.incrementNonces).toHaveBeenCalledWith([1, 2]); }); }); }); diff --git a/src/subscriptions/subscriptions.service.ts b/src/subscriptions/subscriptions.service.ts index bb294fdb..a317116f 100644 --- a/src/subscriptions/subscriptions.service.ts +++ b/src/subscriptions/subscriptions.service.ts @@ -16,9 +16,9 @@ import { SubscriptionStatus } from '~/subscriptions/types'; @Injectable() export class SubscriptionsService { - private ttl: number; - private maxTries: number; - private retryInterval: number; + private readonly ttl: number; + private readonly maxTries: number; + private readonly retryInterval: number; constructor( @Inject(subscriptionsConfig.KEY) config: ConfigType, diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index d329624c..d0e7b3d6 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -32,7 +32,7 @@ export class TransactionsService { * in-memory transaction store by Transaction Identifier. We use a Map * to be able to recycle indexes easily and remove elements in a performant way */ - private transactionStore: Map< + private readonly transactionStore: Map< number, { /** @@ -51,7 +51,7 @@ export class TransactionsService { > = new Map(); private currentId = 0; - private legitimacySecret: string; + private readonly legitimacySecret: string; constructor( @Inject(transactionsConfig.KEY) config: ConfigType,