diff --git a/.vscode/settings.json b/.vscode/settings.json index d3d7b2b1..46a1936e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,4 @@ "[markdown]": { "editor.wordWrap": "off" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 221e6678..933eceba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1420,6 +1420,10 @@ "resolved": "recipes/create-require-from-path", "link": true }, + "node_modules/@nodejs/crypto-fips": { + "resolved": "recipes/crypto-fips", + "link": true + }, "node_modules/@nodejs/import-assertions-to-attributes": { "resolved": "recipes/import-assertions-to-attributes", "link": true @@ -4072,6 +4076,17 @@ "@codemod.com/jssg-types": "^1.0.3" } }, + "recipes/crypto-fips": { + "name": "@nodejs/crypto-fips", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + } + }, "recipes/import-assertions-to-attributes": { "name": "@nodejs/import-assertions-to-attributes", "version": "1.0.0", @@ -4110,7 +4125,7 @@ "@nodejs/codemod-utils": "*" }, "devDependencies": { - "@types/node": "^24.2.1" + "@codemod.com/jssg-types": "^1.0.3" } }, "utils": { @@ -4119,6 +4134,7 @@ "license": "MIT", "devDependencies": { "@ast-grep/napi": "^0.39.3", + "@codemod.com/jssg-types": "^1.0.3", "dedent": "^1.6.0" } } diff --git a/recipes/crypto-fips/README.md b/recipes/crypto-fips/README.md new file mode 100644 index 00000000..5a8bd9ea --- /dev/null +++ b/recipes/crypto-fips/README.md @@ -0,0 +1,45 @@ +# `crypto.fips` DEP0093 + +This recipe transforms the usage from the deprecated `crypto.fips` to `crypto.getFips()` and `crypto.setFips()`. + +See [DEP0093](https://nodejs.org/api/deprecations.html#DEP0093). + +## Examples + +**Before:** + +```js +import crypto from "node:crypto"; +import { fips } from "node:crypto"; + +// Using crypto.fips +crypto.fips; +fips; + +// Using crypto.fips = true +crypto.fips = true; +fips = true; + +// Using crypto.fips = false +crypto.fips = false; +fips = false; +``` + +**After:** + +```js +import crypto from "node:crypto"; +import { getFips, setFips } from "node:crypto"; + +// Using crypto.getFips() +crypto.getFips(); +getFips(); + +// Using crypto.setFips(true) +crypto.setFips(true); +setFips(true); + +// Using crypto.setFips(false) +crypto.setFips(false); +setFips(false); +``` diff --git a/recipes/crypto-fips/codemod.yaml b/recipes/crypto-fips/codemod.yaml new file mode 100644 index 00000000..350ba1a1 --- /dev/null +++ b/recipes/crypto-fips/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/crypto-fips" +version: 1.0.0 +description: Handle DEP0093 via transforming `crypto.fips` to `crypto.getFips()` and `crypto.setFips()` +author: Usman S. +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/crypto-fips/package.json b/recipes/crypto-fips/package.json new file mode 100644 index 00000000..7d885910 --- /dev/null +++ b/recipes/crypto-fips/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/crypto-fips", + "version": "1.0.0", + "description": "Handle DEP0093 via transforming `crypto.fips` to `crypto.getFips()` and `crypto.setFips()`", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/crypto-fips", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Usman S.", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/crypto-fips/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/crypto-fips/src/workflow.ts b/recipes/crypto-fips/src/workflow.ts new file mode 100644 index 00000000..52346fff --- /dev/null +++ b/recipes/crypto-fips/src/workflow.ts @@ -0,0 +1,277 @@ +import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { SgRoot, Edit, SgNode } from '@codemod.com/jssg-types/main'; + +// Escape regexp characters - "crypto.fips" -> "crypto\.fips" +const escapeRegExp = (input: string) => + input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Transform function that converts deprecated crypto.fips calls + * to the new crypto.getFips() and crypto.setFips() syntax. + * + * Handles: + * 1. crypto.fips -> crypto.getFips() + * 2. crypto.fips = true -> crypto.setFips(true) + * 3. crypto.fips = false -> crypto.setFips(false) + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + const cryptoVars = collectCryptoFipsVariables(root); + + for (const [varName, info] of cryptoVars) { + if (info.type === 'member' && info.base) { + edits.push(...replaceMemberAssignments(rootNode, info.base)); + edits.push(...replaceMemberReads(rootNode, info.base)); + } else if (info.type === 'named') { + edits.push(...replaceNamedAssignments(rootNode, varName)); + edits.push(...replaceNamedReads(rootNode, varName)); + } + } + + edits.push(...updateCryptoImportSpecifiers(root)); + edits.push(...updateCryptoRequireDestructuring(root)); + + if (edits.length === 0) return null; + + return rootNode.commitEdits(edits); +} + +/** + * Collect all crypto fips variables + */ +function collectCryptoFipsVariables(root: SgRoot) { + const map = new Map(); + const importNodes = getNodeImportStatements(root, 'crypto'); + const requireNodes = getNodeRequireCalls(root, 'crypto'); + const allStatementNodes = [...importNodes, ...requireNodes]; + + for (const { base, type } of getAllCryptoFipsBases(allStatementNodes)) { + map.set(base, { type, base: type === 'member' ? base : undefined }); + } + + return map; +} + +/** + * Replace member access reads (crypto.fips) + */ +function replaceMemberReads(rootNode: SgNode, base: string) { + const edits: Edit[] = []; + const reads = rootNode.findAll({ + rule: { pattern: `${base}.fips` }, + }); + for (const read of reads) { + edits.push(read.replace(`${base}.getFips()`)); + } + return edits; +} + +/** + * Replace member assignments (crypto.fips = val) + */ +function replaceMemberAssignments(rootNode: SgNode, base: string) { + const edits: Edit[] = []; + const assignments = rootNode.findAll({ + rule: { pattern: `${base}.fips = $VALUE` }, + }); + for (const assign of assignments) { + const valueText = assign.getMatch('VALUE')?.text() ?? ''; + const basePropRegex = new RegExp(`\\b${escapeRegExp(base)}\\.fips\\b`, 'g'); + const transformedValue = valueText.replace( + basePropRegex, + `${base}.getFips()`, + ); + edits.push(assign.replace(`${base}.setFips(${transformedValue})`)); + } + return edits; +} + +/** + * Update import specifiers to include getFips and setFips + */ +function updateCryptoImportSpecifiers(root: SgRoot): Edit[] { + const edits: Edit[] = []; + + const importStmts = getNodeImportStatements(root, 'crypto'); + + for (const stmt of importStmts) { + // import_clause contains default/namespace/named parts + const importClause = stmt.find({ rule: { kind: 'import_clause' } }); + if (!importClause) continue; + + // named_imports = `{ ... }` + const namedImports = importClause.find({ rule: { kind: 'named_imports' } }); + if (!namedImports) continue; // nothing to edit if there is no `{ ... }` + + // All specifiers inside `{ ... }` + const specifiers = namedImports.findAll({ + rule: { kind: 'import_specifier' }, + }); + if (!specifiers || specifiers.length === 0) continue; + + let hasFips = false; + let hasGet = false; + let hasSet = false; + + const keepTexts: string[] = []; + + for (const spec of specifiers) { + // imported name is in field "name" + const importedNameNode = spec.find({ + rule: { has: { field: 'name', kind: 'identifier' } }, + }); + const importedName = importedNameNode + ?.find({ + rule: { kind: 'identifier' }, + }) + ?.text(); + + if (importedName === 'fips') { + hasFips = true; // drop this one; we will add getFips/setFips instead + continue; + } + if (importedName === 'getFips') hasGet = true; + if (importedName === 'setFips') hasSet = true; + + // Preserve other specifiers as-is (including aliases like `name as alias`) + keepTexts.push(spec.text()); + } + + if (!hasFips) continue; // only rewrite when file was importing `fips` + + // Ensure both getFips and setFips are present + if (!hasGet) keepTexts.push('getFips'); + if (!hasSet) keepTexts.push('setFips'); + + // Replace the whole `{ ... }` + edits.push(namedImports.replace(`{ ${keepTexts.join(', ')} }`)); + } + + return edits; +} + +/** + * Update require destructuring to include getFips and setFips + */ +function updateCryptoRequireDestructuring(root: SgRoot): Edit[] { + const edits: Edit[] = []; + + const decls = getNodeRequireCalls(root, 'crypto'); + + for (const decl of decls) { + const objPattern = decl.find({ rule: { kind: 'object_pattern' } }); + if (!objPattern) continue; + + const props = objPattern.findAll({ + rule: { + any: [ + { kind: 'shorthand_property_identifier_pattern' }, // `{ foo }` + { kind: 'pair_pattern' }, // `{ foo: bar }` + ], + }, + }); + if (!props || props.length === 0) continue; + + let hasFips = false; + let hasGet = false; + let hasSet = false; + + const keepTexts: string[] = []; + + for (const p of props) { + if (p.kind() === 'shorthand_property_identifier_pattern') { + const name = p.text().trim(); + if (name === 'fips') { + hasFips = true; + continue; + } + if (name === 'getFips') hasGet = true; + if (name === 'setFips') hasSet = true; + keepTexts.push(name); + } else { + // pair_pattern: has key + value (e.g. `fips: myFips`, `getFips: gf`) + const keyNode = p.find({ rule: { kind: 'property_identifier' } }); + const key = keyNode?.text(); + + if (key === 'fips') { + hasFips = true; // drop any alias of fips + continue; + } + if (key === 'getFips') hasGet = true; + if (key === 'setFips') hasSet = true; + + // Keep other pairs as-is (preserves aliasing/spacing nicely) + keepTexts.push(p.text().trim()); + } + } + + if (!hasFips) continue; // only rewrite when it actually destructured `fips` + + if (!hasGet) keepTexts.push('getFips'); + if (!hasSet) keepTexts.push('setFips'); + + edits.push(objPattern.replace(`{ ${keepTexts.join(', ')} }`)); + } + + return edits; +} + +/** + * Replace named reads (fips -> getFips()) + */ +function replaceNamedReads(rootNode: SgNode, varName: string) { + const edits: Edit[] = []; + const reads = rootNode.findAll({ + rule: { pattern: varName }, + }); + for (const read of reads) { + edits.push(read.replace('getFips()')); + } + return edits; +} + +/** + * Replace named assignments (fips = val -> setFips(val)) + */ +function replaceNamedAssignments(rootNode: SgNode, varName: string) { + const edits: Edit[] = []; + const assignments = rootNode.findAll({ + rule: { pattern: `${varName} = $VALUE` }, + }); + for (const assign of assignments) { + const valueText = assign.getMatch('VALUE')?.text() ?? ''; + edits.push(assign.replace(`setFips(${valueText})`)); + } + return edits; +} + +/** + * Get the base of the crypto fips variable + * import { fips } from "node:crypto" -> "fips" + * import crypto from "node:crypto" -> "crypto" + * const { fips } = require("node:crypto") -> "fips" + * const crypto = require("node:crypto") -> "crypto" + */ +function* getCryptoFipsBase(statements: SgNode[], type: 'member' | 'named') { + for (const stmt of statements) { + const resolvedPath = resolveBindingPath(stmt, '$.fips'); + if (resolvedPath?.includes('.') && type === 'member') { + const base = resolvedPath.slice(0, resolvedPath.lastIndexOf('.')); + yield { base, type }; + } else if (resolvedPath && type === 'named') { + yield { base: resolvedPath, type }; + } + } +} + +/** + * Get crypto bases/names for both member and named imports + */ +function* getAllCryptoFipsBases(statements: SgNode[]) { + yield* getCryptoFipsBase(statements, 'member'); + yield* getCryptoFipsBase(statements, 'named'); +} diff --git a/recipes/crypto-fips/tests/expected/file-1.js b/recipes/crypto-fips/tests/expected/file-1.js new file mode 100644 index 00000000..05341284 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-1.js @@ -0,0 +1,5 @@ +const crypto = require("node:crypto"); + +if (crypto.getFips()) { + console.log("FIPS mode is enabled"); +} diff --git a/recipes/crypto-fips/tests/expected/file-10.js b/recipes/crypto-fips/tests/expected/file-10.js new file mode 100644 index 00000000..9ef80aab --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-10.js @@ -0,0 +1,6 @@ +import { getFips, setFips } from "node:crypto"; + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-2.js b/recipes/crypto-fips/tests/expected/file-2.js new file mode 100644 index 00000000..a773caaa --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-2.js @@ -0,0 +1,3 @@ +const crypto = require("node:crypto"); + +crypto.setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-3.js b/recipes/crypto-fips/tests/expected/file-3.js new file mode 100644 index 00000000..7c18c4e3 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-3.js @@ -0,0 +1,6 @@ +const crypto = require("node:crypto"); + +if (process.env.ENABLE_FIPS === "true") { + crypto.setFips(true); +} +console.log("FIPS enabled:", crypto.getFips()); diff --git a/recipes/crypto-fips/tests/expected/file-4.js b/recipes/crypto-fips/tests/expected/file-4.js new file mode 100644 index 00000000..035297c2 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-4.js @@ -0,0 +1,4 @@ +import crypto from "node:crypto"; + +const fipsStatus = crypto.getFips(); +crypto.setFips(!fipsStatus); diff --git a/recipes/crypto-fips/tests/expected/file-5.js b/recipes/crypto-fips/tests/expected/file-5.js new file mode 100644 index 00000000..9663bd66 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-5.js @@ -0,0 +1,4 @@ +const nodeCrypto = require("node:crypto"); + +const currentFips = nodeCrypto.getFips(); +nodeCrypto.setFips(!currentFips); diff --git a/recipes/crypto-fips/tests/expected/file-6.js b/recipes/crypto-fips/tests/expected/file-6.js new file mode 100644 index 00000000..1c0f1aa9 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-6.js @@ -0,0 +1,4 @@ +const crypto = require("node:crypto"); + +console.log("FIPS enabled:", crypto.getFips()); +crypto.setFips(crypto.getFips() || process.env.FORCE_FIPS); diff --git a/recipes/crypto-fips/tests/expected/file-7.js b/recipes/crypto-fips/tests/expected/file-7.js new file mode 100644 index 00000000..0c668e48 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-7.js @@ -0,0 +1,6 @@ +const { getFips, setFips } = require("node:crypto"); + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-8.js b/recipes/crypto-fips/tests/expected/file-8.js new file mode 100644 index 00000000..0c668e48 --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-8.js @@ -0,0 +1,6 @@ +const { getFips, setFips } = require("node:crypto"); + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/expected/file-9.js b/recipes/crypto-fips/tests/expected/file-9.js new file mode 100644 index 00000000..9ef80aab --- /dev/null +++ b/recipes/crypto-fips/tests/expected/file-9.js @@ -0,0 +1,6 @@ +import { getFips, setFips } from "node:crypto"; + +if (getFips()) { + console.log("FIPS mode is enabled"); +} +setFips(true); diff --git a/recipes/crypto-fips/tests/input/file-1.js b/recipes/crypto-fips/tests/input/file-1.js new file mode 100644 index 00000000..867856b1 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-1.js @@ -0,0 +1,5 @@ +const crypto = require("node:crypto"); + +if (crypto.fips) { + console.log("FIPS mode is enabled"); +} diff --git a/recipes/crypto-fips/tests/input/file-10.js b/recipes/crypto-fips/tests/input/file-10.js new file mode 100644 index 00000000..f59cec0f --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-10.js @@ -0,0 +1,6 @@ +import { fips as fipsRenamed } from "node:crypto"; + +if (fipsRenamed) { + console.log("FIPS mode is enabled"); +} +fipsRenamed = true; diff --git a/recipes/crypto-fips/tests/input/file-2.js b/recipes/crypto-fips/tests/input/file-2.js new file mode 100644 index 00000000..792d2317 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-2.js @@ -0,0 +1,3 @@ +const crypto = require("node:crypto"); + +crypto.fips = true; diff --git a/recipes/crypto-fips/tests/input/file-3.js b/recipes/crypto-fips/tests/input/file-3.js new file mode 100644 index 00000000..f5800590 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-3.js @@ -0,0 +1,6 @@ +const crypto = require("node:crypto"); + +if (process.env.ENABLE_FIPS === "true") { + crypto.fips = true; +} +console.log("FIPS enabled:", crypto.fips); diff --git a/recipes/crypto-fips/tests/input/file-4.js b/recipes/crypto-fips/tests/input/file-4.js new file mode 100644 index 00000000..50caa598 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-4.js @@ -0,0 +1,4 @@ +import crypto from "node:crypto"; + +const fipsStatus = crypto.fips; +crypto.fips = !fipsStatus; diff --git a/recipes/crypto-fips/tests/input/file-5.js b/recipes/crypto-fips/tests/input/file-5.js new file mode 100644 index 00000000..aeca0226 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-5.js @@ -0,0 +1,4 @@ +const nodeCrypto = require("node:crypto"); + +const currentFips = nodeCrypto.fips; +nodeCrypto.fips = !currentFips; diff --git a/recipes/crypto-fips/tests/input/file-6.js b/recipes/crypto-fips/tests/input/file-6.js new file mode 100644 index 00000000..0131d320 --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-6.js @@ -0,0 +1,4 @@ +const crypto = require("node:crypto"); + +console.log("FIPS enabled:", crypto.fips); +crypto.fips = crypto.fips || process.env.FORCE_FIPS; diff --git a/recipes/crypto-fips/tests/input/file-7.js b/recipes/crypto-fips/tests/input/file-7.js new file mode 100644 index 00000000..d2a79f8f --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-7.js @@ -0,0 +1,6 @@ +const { fips } = require("node:crypto"); + +if (fips) { + console.log("FIPS mode is enabled"); +} +fips = true; diff --git a/recipes/crypto-fips/tests/input/file-8.js b/recipes/crypto-fips/tests/input/file-8.js new file mode 100644 index 00000000..c21c960c --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-8.js @@ -0,0 +1,6 @@ +const { fips: fipsRenamed } = require("node:crypto"); + +if (fipsRenamed) { + console.log("FIPS mode is enabled"); +} +fipsRenamed = true; diff --git a/recipes/crypto-fips/tests/input/file-9.js b/recipes/crypto-fips/tests/input/file-9.js new file mode 100644 index 00000000..1cac100c --- /dev/null +++ b/recipes/crypto-fips/tests/input/file-9.js @@ -0,0 +1,6 @@ +import { fips } from "node:crypto"; + +if (fips) { + console.log("FIPS mode is enabled"); +} +fips = true; diff --git a/recipes/crypto-fips/tsconfig.json b/recipes/crypto-fips/tsconfig.json new file mode 100644 index 00000000..92c12497 --- /dev/null +++ b/recipes/crypto-fips/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowJs": true, + "alwaysStrict": true, + "baseUrl": "./", + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "lib": ["ESNext", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noImplicitThis": true, + "removeComments": true, + "strict": true, + "stripInternal": true, + "target": "esnext" + }, + "include": ["./"], + "exclude": [ + "tests/**" + ] +} diff --git a/recipes/crypto-fips/workflow.yaml b/recipes/crypto-fips/workflow.yaml new file mode 100644 index 00000000..705eed97 --- /dev/null +++ b/recipes/crypto-fips/workflow.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: Handle DEP0093 via transforming `crypto.fips` to `crypto.getFips()`, `crypto.setFips()` to `crypto.setFips(true)` and `crypto.setFips(false)`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript