Files
Satoshi Qazi Muhammed c4f2bd943b Lock the indexer image and close the advisories in the build tooling
The indexer image installed @subql/node and the @polkadot -> @pezkuwi aliases
with an unlocked npm install, and the build cache kept the layer from the day
it was first built: the indexers ran @pezkuwi/wasm-crypto 7.5.17/7.5.18, the
build whose ed25519 and secp256k1 are stubs, plus vm2 3.9.19 (the mapping
sandbox, 54 advisories). No lockfile meant Dependabot never saw that tree.

- docker/indexer holds the image's package.json and package-lock.json:
  @subql/node 6.4.6 as today, @pezkuwi 16.5.39 / 14.0.26 / 7.5.21, overrides
  for vm2 3.12.2, uuid 11 and package-json 7 (got 11). The Dockerfile runs
  npm ci on Node 24 (Node 20 is out of support).
- The pruned-state patch stops the build when a pattern matches nothing,
  instead of reporting success with the fallback missing.
- CI builds the indexer image and runs the crypto smoke test inside it
  through the @polkadot aliases, the way subql-node loads them.
- The root yarn.lock, which the host installs as root on every deploy to run
  subql codegen/build, moves the advisory ranges forward with targeted
  resolutions and @pezkuwi/braces. Left: elliptic (low, via ethers 5 in
  @subql/cli) and sprintf-js (moderate, via jest's argparse 1); neither has
  a fixed release.
2026-10-12 17:14:07 +04:00

77 lines
2.9 KiB
JavaScript

// Runs from a bot's directory (in its image: mounted at /app/bot-smoke.mjs) and fails unless the
// installed crypto stack is the one package-lock.json records and can sign and
// verify with every key type. A stack that only loads is not enough: the broken
// @pezkuwi/wasm-crypto 7.5.17 loaded fine and failed at ed25519 and ecdsa.
//
// node bot-smoke.mjs <esm|cjs|indexer>
//
// indexer: the subql-node image, which loads @pezkuwi through @polkadot aliases
// (CommonJS); the aliases are checked against the lock too.
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const mode = process.argv[2];
const require = createRequire(import.meta.url);
const dir = path.dirname(fileURLToPath(import.meta.url));
let failed = 0;
function check (name, ok, detail = '') {
console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ` (${detail})` : ''}`);
if (!ok) {
failed++;
}
}
const lock = JSON.parse(fs.readFileSync(path.join(dir, 'package-lock.json'), 'utf8'));
const scope = mode === 'indexer' ? '@polkadot' : '@pezkuwi';
const locked = ['@pezkuwi/wasm-crypto', '@pezkuwi/util-crypto', '@pezkuwi/api', '@pezkuwi/keyring'];
if (mode === 'indexer') {
locked.push('@polkadot/wasm-crypto', '@polkadot/util-crypto', '@polkadot/api', '@polkadot/keyring');
}
for (const name of locked) {
const want = lock.packages[`node_modules/${name}`]?.version;
const have = JSON.parse(fs.readFileSync(path.join(dir, 'node_modules', name, 'package.json'), 'utf8')).version;
check(`${name} is the locked version`, !!want && want === have, `lock ${want}, installed ${have}`);
}
// Load the stack the way the image does: the noter is an ES module, the payout
// bot and subql-node are CommonJS.
const load = (name) => mode === 'esm' ? import(`${scope}/${name}`) : require(`${scope}/${name}`);
const { Keyring } = await load('api');
const { cryptoWaitReady, mnemonicGenerate } = await load('util-crypto');
check('crypto initialises', await cryptoWaitReady());
try {
check('a mnemonic can be generated', mnemonicGenerate().split(' ').length === 12);
} catch (error) {
check('a mnemonic can be generated', false, error.message);
}
// Derived from the public development phrase, so the check needs no secret
// and does not depend on mnemonic generation working.
const message = new TextEncoder().encode('pezkuwi bot smoke');
for (const type of ['sr25519', 'ed25519', 'ecdsa']) {
try {
const pair = new Keyring({ type }).addFromUri('//Smoke');
const signature = pair.sign(message);
check(`${type} signs and verifies`, pair.verify(message, signature, pair.publicKey));
check(`${type} rejects a tampered message`, !pair.verify(new TextEncoder().encode('tampered'), signature, pair.publicKey));
} catch (error) {
check(`${type} signs and verifies`, false, error.message);
}
}
console.log(failed ? `FAILED: ${failed}` : `all ${mode} smoke checks passed`);
process.exit(failed ? 1 : 0);