lint: ban raw HTTP2 imports

This commit is contained in:
jesse-merhi
2026-05-04 02:30:39 +10:00
committed by clawsweeper
parent 590a474a66
commit bfa0ca09e3
3 changed files with 126 additions and 1 deletions

View File

@@ -1420,12 +1420,18 @@
"lint:plugins:no-monolithic-plugin-sdk-entry-imports": "node --import tsx scripts/check-no-monolithic-plugin-sdk-entry-imports.ts",
"lint:plugins:no-register-http-handler": "node scripts/check-no-register-http-handler.mjs",
"lint:plugins:plugin-sdk-subpaths-exported": "node scripts/check-plugin-sdk-subpath-exports.mjs",
<<<<<<< HEAD
"lint:scripts": "pnpm lint:docker-e2e && node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.scripts.json scripts",
"lint:swift": "swiftlint lint --config config/swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
=======
"lint:scripts": "pnpm lint:docker-e2e && pnpm lint:tmp:no-raw-http2-imports && node scripts/run-oxlint.mjs --tsconfig tsconfig.oxlint.scripts.json scripts",
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
>>>>>>> 59b4c14509 (lint: ban raw HTTP2 imports)
"lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs",
"lint:tmp:dynamic-import-warts": "node scripts/check-dynamic-import-warts.mjs",
"lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs",
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
"lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs",
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",
"lint:web-fetch-provider-boundaries": "node scripts/check-web-fetch-provider-boundaries.mjs",

View File

@@ -0,0 +1,120 @@
import fs from "node:fs";
import path from "node:path";
const SOURCE_ROOTS = ["src", "extensions"];
const DEFAULT_SKIPPED_DIR_NAMES = new Set(["node_modules", "dist", "coverage", ".generated"]);
function isCodeFile(filePath) {
if (filePath.endsWith(".d.ts")) {
return false;
}
return /\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(filePath);
}
function collectFilesSync(rootDir, options) {
const skipDirNames = options.skipDirNames ?? DEFAULT_SKIPPED_DIR_NAMES;
const files = [];
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
let entries = [];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
if (!skipDirNames.has(entry.name)) {
stack.push(fullPath);
}
continue;
}
if (entry.isFile() && options.includeFile(fullPath)) {
files.push(fullPath);
}
}
}
return files;
}
function toPosixPath(filePath) {
return filePath.replaceAll("\\", "/");
}
const FORBIDDEN_HTTP2_MODULES = new Set(["node:http2", "http2"]);
const ALLOWED_PRODUCTION_FILES = new Set(["src/infra/push-apns-http2.ts"]);
function isTestFile(relativePath) {
return (
/(?:^|\/)(?:test|test-fixtures)\//u.test(relativePath) ||
/\.test\.[cm]?[jt]sx?$/u.test(relativePath)
);
}
function lineNumberForOffset(content, offset) {
return content.slice(0, offset).split(/\r?\n/u).length;
}
function collectHttp2ImportOffenders(filePath) {
const relativePath = toPosixPath(path.relative(process.cwd(), filePath));
if (ALLOWED_PRODUCTION_FILES.has(relativePath) || isTestFile(relativePath)) {
return [];
}
const content = fs.readFileSync(filePath, "utf8");
const offenders = [];
const patterns = [
/\bimport\s+(?:type\s+)?[\s\S]*?\bfrom\s*["']([^"']+)["']/gu,
/\bexport\s+(?:type\s+)?[\s\S]*?\bfrom\s*["']([^"']+)["']/gu,
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/gu,
/\brequire\s*\(\s*["']([^"']+)["']\s*\)/gu,
];
for (const pattern of patterns) {
for (const match of content.matchAll(pattern)) {
const specifier = match[1];
if (specifier && FORBIDDEN_HTTP2_MODULES.has(specifier)) {
offenders.push({
file: relativePath,
line: lineNumberForOffset(content, match.index ?? 0),
specifier,
});
}
}
}
return offenders;
}
function collectSourceFiles() {
return SOURCE_ROOTS.flatMap((root) =>
collectFilesSync(path.join(process.cwd(), root), {
includeFile: isCodeFile,
}),
);
}
function main() {
const offenders = collectSourceFiles().flatMap(collectHttp2ImportOffenders);
if (offenders.length === 0) {
console.log("OK: raw node:http2 imports stay behind the APNs proxy wrapper.");
return;
}
console.error("Raw node:http2 imports are only allowed in src/infra/push-apns-http2.ts.");
for (const offender of offenders.toSorted(
(a, b) => a.file.localeCompare(b.file) || a.line - b.line,
)) {
console.error(`- ${offender.file}:${offender.line} imports ${offender.specifier}`);
}
console.error("Use connectApnsHttp2Session() so APNs HTTP/2 honors managed proxy policy.");
process.exit(1);
}
main();

View File

@@ -1,6 +1,5 @@
import { createHash, createPrivateKey, sign as signJwt } from "node:crypto";
import fs from "node:fs/promises";
import http2 from "node:http2";
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import {