Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Stage 1 — Install dependencies
FROM node:24.21.0-bookworm-slim AS deps
RUN npm install -g pnpm@12.3.2
WORKDIR /app
COPY scripts/install-verified.mjs ./scripts/
RUN node scripts/install-verified.mjs pnpm@12.3.2
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile

# Stage 2 — Build TypeScript and prune dev dependencies
FROM node:24.21.0-bookworm-slim AS build
RUN npm install -g pnpm@12.3.2
WORKDIR /app
COPY scripts/install-verified.mjs ./scripts/
RUN node scripts/install-verified.mjs pnpm@12.3.2
COPY --from=deps /app/node_modules ./node_modules
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json ./
COPY src/ ./src/
Expand Down
7 changes: 6 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"description": "Track the pnpm version installed in the Dockerfile.",
"customType": "regex",
"managerFilePatterns": ["/^Dockerfile$/"],
"matchStrings": ["npm install -g (?<depName>pnpm)@(?<currentValue>\\S+)"],
"matchStrings": ["install-verified\\.mjs (?<depName>pnpm)@(?<currentValue>\\S+)"],
"datasourceTemplate": "npm"
}
],
Expand All @@ -22,6 +22,11 @@
"description": "Keep the Node version in .node-version and the Dockerfile in one PR.",
"matchPackageNames": ["node"],
"groupName": "node"
},
{
"description": "Keep the pnpm version in package.json and the Dockerfile in one PR.",
"matchPackageNames": ["pnpm"],
"groupName": "pnpm"
}
]
}
51 changes: 51 additions & 0 deletions scripts/install-verified.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createHash, createVerify } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const REGISTRY = 'https://registry.npmjs.org';

const spec = process.argv[2];
if (!spec || !spec.includes('@')) {
console.error('usage: install-verified.mjs <package>@<version>');
process.exit(1);
}

const at = spec.lastIndexOf('@');
const name = spec.slice(0, at);
const version = spec.slice(at + 1);

async function getJson(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`${url} responded ${response.status}`);
return response.json();
}

function toPem(key) {
return `-----BEGIN PUBLIC KEY-----\n${key.match(/.{1,64}/g).join('\n')}\n-----END PUBLIC KEY-----`;
}

const { dist } = await getJson(`${REGISTRY}/${name}/${version}`);
const signature = dist.signatures?.[0];
if (!signature) throw new Error(`${spec} has no registry signature`);

const { keys } = await getJson(`${REGISTRY}/-/npm/v1/keys`);
const signingKey = keys.find((candidate) => candidate.keyid === signature.keyid);
if (!signingKey) throw new Error(`no registry key for ${signature.keyid}`);

const signed = createVerify('SHA256')
.update(`${name}@${version}:${dist.integrity}`)
.verify(toPem(signingKey.key), signature.sig, 'base64');
if (!signed) throw new Error(`${spec} failed registry signature verification`);

const tarball = Buffer.from(await (await fetch(dist.tarball)).arrayBuffer());
const digest = `sha512-${createHash('sha512').update(tarball).digest('base64')}`;
if (digest !== dist.integrity)
throw new Error(`${spec} tarball does not match the signed integrity`);

const path = join(tmpdir(), `${name.replace('/', '-')}-${version}.tgz`);
writeFileSync(path, tarball);
execFileSync('npm', ['install', '-g', path], { stdio: 'inherit' });

console.log(`verified and installed ${spec}`);
File renamed without changes.