-
-
Notifications
You must be signed in to change notification settings - Fork 31
feat(tls-securepair-to-tlssocket): introduce #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6ef1d20
1c4fe70
c638006
485fb15
3e43dea
3e78f7b
2a069ed
52de208
b12b649
fe681b9
153faf9
40986bd
0cd5eee
af0f097
a2b7796
6f891fe
9d501a2
8fbb180
9cdb7cc
91216ec
4148701
700224b
28a3357
9bcc4d9
b6a95fa
62a72f7
0a1f785
8ffc05c
b18b065
9c54544
d22e788
be3ccee
43324bf
9b0dd42
df79099
2560ceb
8fd0b6c
9d3224c
23e8baa
de37948
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| # tls-securepair-to-tlssocket | ||
|
|
||
| Codemod to migrate from the deprecated `tls.SecurePair` class to `tls.TLSSocket` in Node.js applications. `SecurePair` was deprecated and subsequently removed in favor of `TLSSocket`. | ||
|
|
||
| ## What it does | ||
|
|
||
| This codemod transforms usages of `tls.SecurePair` into `tls.TLSSocket`. Since `TLSSocket` wraps an existing socket, the codemod injects a `socket` argument that you may need to define or bind in your context. | ||
|
|
||
| Key transformations: | ||
| - **Constructor:** Replaces `new SecurePair()` with `new TLSSocket(socket)`. | ||
| - **Imports:** Updates `require` and `import` statements from `SecurePair` to `TLSSocket`. | ||
| - **Renaming:** Intelligently renames variables (e.g., `pair` → `socket`, `securePairInstance` → `socketInstance`) while preserving CamelCase. | ||
| - **Cleanup:** Removes deprecated property accesses like `.cleartext` and `.encrypted`. | ||
| - **Annotations:** Adds comments to highlight where manual API verification is needed. | ||
|
|
||
| ## Supports | ||
|
|
||
| - **Module Systems:** | ||
| - CommonJS: `const tls = require('node:tls')` / `const { SecurePair } = ...` | ||
| - ESM: `import tls from 'node:tls'` / `import { SecurePair } ...` | ||
| - **Variable Renaming:** | ||
| - Updates variable declarations: `const pair = ...` → `const socket = ...` | ||
| - Updates references deep in the scope: `pair.on('error')` → `socket.on('error')` | ||
| - Handles naming variations: `myPair` → `mySocket`, `securePair` → `secureSocket`. | ||
| - **Cleanup:** | ||
| - Identifies and removes lines accessing `cleartext` or `encrypted` properties. | ||
| - **Namespace Handling:** | ||
| - Supports both `new tls.SecurePair()` and `new SecurePair()`. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Case 1: CommonJS with namespace access | ||
|
|
||
| ```diff | ||
| const tls = require('node:tls'); | ||
|
|
||
| - const pair = new tls.SecurePair(); | ||
| - const encrypted = pair.encrypted; | ||
| + const socket = new tls.TLSSocket(socket); | ||
| ``` | ||
|
|
||
| ### Case 2: ESM with destructuring | ||
|
|
||
| ```diff | ||
| - import { SecurePair } from 'node:tls'; | ||
| + import { TLSSocket } from 'node:tls'; | ||
|
|
||
| - const myPair = new SecurePair(); | ||
| - myPair.cleartext.write('hello'); | ||
| + const mySocket = new TLSSocket(socket); | ||
| + mySocket.write('hello'); | ||
| ``` | ||
|
|
||
| ### Case 3: Variable renaming across scope | ||
|
|
||
| ```diff | ||
| const tls = require('node:tls'); | ||
|
|
||
| - const securePair = new tls.SecurePair(); | ||
| + const secureSocket = new tls.TLSSocket(socket); | ||
|
|
||
| - securePair.on('error', (err) => { | ||
| + secureSocket.on('error', (err) => { | ||
| console.error(err); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Case 4: Multiple variables with cleanup | ||
|
|
||
| ```diff | ||
| - const { SecurePair } = require('node:tls'); | ||
| + const { TLSSocket } = require('node:tls'); | ||
|
|
||
| - const pair = new SecurePair(); | ||
| - const cleartext = pair.cleartext; | ||
| + const socket = new TLSSocket(socket); | ||
| ``` | ||
|
|
||
| ## Warning | ||
|
|
||
| The tls.TLSSocket constructor requires an existing socket instance (net.Socket) as an argument. This codemod automatically inserts socket as the argument: | ||
| JavaScript | ||
|
|
||
| ```js | ||
| ``` | ||
menace31 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| new TLSSocket(socket) | ||
| ``` | ||
|
|
||
| You must ensure that a variable named socket exists in the scope or rename it to match your existing socket variable (e.g., clientSocket, stream, etc.). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| schema_version: "1.0" | ||
| name: "@nodejs/tls-securepair-to-tlssocket" | ||
| version: "1.0.0" | ||
| description: Migrate usages of `tls.SecurePair` to `tls.TLSSocket` where possible. | ||
| author: Maxime Devillet | ||
| license: MIT | ||
| workflow: workflow.yaml | ||
| category: migration | ||
|
|
||
| targets: | ||
| languages: | ||
| - javascript | ||
| - typescript | ||
|
|
||
| keywords: | ||
| - transformation | ||
| - migration | ||
| - tls | ||
| - securepair | ||
|
|
||
| registry: | ||
| access: public | ||
| visibility: public |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "@nodejs/tls-securepair-to-tlssocket", | ||
| "version": "1.0.0", | ||
| "description": "Migrate usages of tls.SecurePair to tls.TLSSocket.", | ||
| "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/tls-securepair-to-tlssocket", | ||
| "bugs": "https://github.com/nodejs/userland-migrations/issues" | ||
| }, | ||
| "author": "Maxime Devillet", | ||
| "license": "MIT", | ||
| "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/tls-securepair-to-tlssocket/README.md", | ||
| "devDependencies": { | ||
| "@codemod.com/jssg-types": "^1.0.9" | ||
| }, | ||
| "dependencies": { | ||
| "@nodejs/codemod-utils": "*", | ||
| "@ast-grep/napi": "^0.40.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,159 @@ | ||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||
| getNodeImportStatements, | ||||||||||||||||||||||||||
| getNodeImportCalls, | ||||||||||||||||||||||||||
| } from '@nodejs/codemod-utils/ast-grep/import-statement'; | ||||||||||||||||||||||||||
| import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; | ||||||||||||||||||||||||||
| import { updateBinding } from '@nodejs/codemod-utils/ast-grep/update-binding'; | ||||||||||||||||||||||||||
| import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines'; | ||||||||||||||||||||||||||
| import { getScope } from '@nodejs/codemod-utils/ast-grep/get-scope'; | ||||||||||||||||||||||||||
| import type { Edit, SgRoot, Range, SgNode } from '@codemod.com/jssg-types/main'; | ||||||||||||||||||||||||||
| import type Js from '@codemod.com/jssg-types/langs/javascript'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Codemod: migrate SecurePair -> TLSSocket | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function getClosest(node: SgNode<Js>, kinds: string[]): SgNode<Js> | null { | ||||||||||||||||||||||||||
| // Prefer shared `getScope` helper when a single kind is requested. | ||||||||||||||||||||||||||
| if (kinds.length === 1) return getScope(node, kinds[0]) ?? null; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Walk up the ancestor chain and return the first matching kind. | ||||||||||||||||||||||||||
| let current = node.parent(); | ||||||||||||||||||||||||||
| while (current) { | ||||||||||||||||||||||||||
| if (kinds.includes(current.kind())) return current; | ||||||||||||||||||||||||||
| current = current.parent(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Utility: find the closest ancestor whose kind is one of `kinds`. | ||||||||||||||||||||||||||
| // Returns the first matching ancestor or `null` when none is found. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export default function transform(root: SgRoot<Js>): string | null { | ||||||||||||||||||||||||||
| const rootNode = root.root(); | ||||||||||||||||||||||||||
| const edits: Edit[] = []; | ||||||||||||||||||||||||||
| const linesToRemove: Range[] = []; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Collect all import/require nodes that reference the `tls` module. | ||||||||||||||||||||||||||
| // This includes static imports, require() calls and dynamic imports. | ||||||||||||||||||||||||||
| const importNodes = [ | ||||||||||||||||||||||||||
| ...getNodeImportStatements(root, 'tls'), | ||||||||||||||||||||||||||
| ...getNodeRequireCalls(root, 'tls'), | ||||||||||||||||||||||||||
| ...getNodeImportCalls(root, 'tls'), | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (const node of importNodes) { | ||||||||||||||||||||||||||
| // Update any binding that imports SecurePair -> TLSSocket (e.g. import { SecurePair }) | ||||||||||||||||||||||||||
| const change = updateBinding(node, { old: 'SecurePair', new: 'TLSSocket' }); | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can trust the name of the import will be 'SecurePair', it can be renamed while importing. so use our utility const binding = resolveBindingPath(node, '$.SecurePair')
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here is an example of resolveBindingPath usage userland-migrations/recipes/util-log-to-console-log/src/workflow.ts Lines 48 to 59 in 1195993
|
||||||||||||||||||||||||||
| if (change?.edit) edits.push(change.edit); | ||||||||||||||||||||||||||
| if (change?.lineToRemove) linesToRemove.push(change.lineToRemove); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Find `new` expressions that construct a SecurePair either via namespace | ||||||||||||||||||||||||||
| // (e.g. `tls.SecurePair`) or via a direct identifier (`SecurePair`). | ||||||||||||||||||||||||||
| const newExpressions = rootNode.findAll({ | ||||||||||||||||||||||||||
| rule: { | ||||||||||||||||||||||||||
| kind: 'new_expression', | ||||||||||||||||||||||||||
| has: { | ||||||||||||||||||||||||||
| any: [ | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| kind: 'member_expression', | ||||||||||||||||||||||||||
| has: { field: 'property', regex: '^SecurePair$' }, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| { kind: 'identifier', pattern: 'SecurePair' }, | ||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (const node of newExpressions) { | ||||||||||||||||||||||||||
| const callee = node.field('constructor'); | ||||||||||||||||||||||||||
| if (!callee) continue; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let newConstructorName = 'TLSSocket'; | ||||||||||||||||||||||||||
| if (callee.kind() === 'member_expression') { | ||||||||||||||||||||||||||
| const object = callee.field('object'); | ||||||||||||||||||||||||||
| if (object) { | ||||||||||||||||||||||||||
| newConstructorName = `${object.text()}.TLSSocket`; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Replace the constructor call with `new TLSSocket(socket)`. | ||||||||||||||||||||||||||
| edits.push(node.replace(`new ${newConstructorName}(socket)`)); | ||||||||||||||||||||||||||
| const declarator = getClosest(node, ['variable_declarator']); | ||||||||||||||||||||||||||
| if (declarator) { | ||||||||||||||||||||||||||
| const idNode = declarator.field('name'); | ||||||||||||||||||||||||||
| if (idNode) { | ||||||||||||||||||||||||||
| const oldName = idNode.text(); | ||||||||||||||||||||||||||
| let newName = 'socket'; | ||||||||||||||||||||||||||
| if (oldName !== 'pair' && oldName !== 'SecurePair') { | ||||||||||||||||||||||||||
| if (oldName.includes('Pair')) | ||||||||||||||||||||||||||
| newName = oldName.replace('Pair', 'Socket'); | ||||||||||||||||||||||||||
| else if (oldName.includes('pair')) | ||||||||||||||||||||||||||
| newName = oldName.replace('pair', 'socket'); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Find usages like `pair.cleartext` or `pair.encrypted` to remove those | ||||||||||||||||||||||||||
| // statements as they don't apply to TLSSocket-based code. | ||||||||||||||||||||||||||
| const obsoleteUsages = rootNode.findAll({ | ||||||||||||||||||||||||||
| rule: { | ||||||||||||||||||||||||||
| kind: 'member_expression', | ||||||||||||||||||||||||||
| all: [ | ||||||||||||||||||||||||||
| { has: { field: 'object', regex: `^${oldName}$` } }, | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| { has: { field: 'property', regex: '^(cleartext|encrypted)$' } }, | ||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| for (const usage of obsoleteUsages) { | ||||||||||||||||||||||||||
| const statement = getClosest(usage, [ | ||||||||||||||||||||||||||
| 'lexical_declaration', | ||||||||||||||||||||||||||
| 'expression_statement', | ||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||
| if (statement) linesToRemove.push(statement.range()); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Rename the variable (e.g. `pair` -> `socket`) and update references. | ||||||||||||||||||||||||||
| edits.push(idNode.replace(newName)); | ||||||||||||||||||||||||||
| const references = rootNode.findAll({ | ||||||||||||||||||||||||||
| rule: { kind: 'identifier', regex: `^${oldName}$` }, | ||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Update all references, skipping property accesses and imports. | ||||||||||||||||||||||||||
| for (const ref of references) { | ||||||||||||||||||||||||||
| const parent = ref.parent(); | ||||||||||||||||||||||||||
| if (!parent) continue; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const parentKind = parent.kind(); | ||||||||||||||||||||||||||
| if (parentKind === 'member_expression') { | ||||||||||||||||||||||||||
| const property = parent.field('property'); | ||||||||||||||||||||||||||
| if (property && property.id() === ref.id()) continue; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||
| parentKind === 'import_specifier' || | ||||||||||||||||||||||||||
| parentKind === 'shorthand_property_identifier_pattern' | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||
| if (ref.id() === idNode.id()) continue; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| edits.push(ref.replace(newName)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const sourceCode = rootNode.commitEdits(edits); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let output = removeLines(sourceCode, linesToRemove) ?? ''; | ||||||||||||||||||||||||||
| // Normalize newlines and trim trailing whitespace for predictable snapshots. | ||||||||||||||||||||||||||
| output = output.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | ||||||||||||||||||||||||||
| output = output.replace(/(^.*\S)[ \t]+$/gm, '$1'); | ||||||||||||||||||||||||||
| output = output.replace(/^\uFEFF/, ''); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const eol = | ||||||||||||||||||||||||||
| typeof process !== 'undefined' && process.platform === 'win32' | ||||||||||||||||||||||||||
| ? '\r\n' | ||||||||||||||||||||||||||
| : '\n'; | ||||||||||||||||||||||||||
| output = output.replace(/\n/g, eol); | ||||||||||||||||||||||||||
| if (output.endsWith(eol)) output = output.slice(0, -eol.length); | ||||||||||||||||||||||||||
|
Comment on lines
+147
to
+156
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t think it’s worth keeping this there if it’s only for Windows tests. We can find another solution for that. |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return output; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| const tls = require('node:tls'); | ||
| const fs = require('fs'); // Code non lié | ||
menace31 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function createSecureConnection() { | ||
| // Using tls.SecurePair constructor | ||
| const socket = new tls.TLSSocket(socket); | ||
|
|
||
| // Ces lignes doivent disparaître | ||
|
|
||
| return socket; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| const tls = require('node:tls'); | ||
|
|
||
| // Using tls.SecurePair constructor | ||
| const socket = new tls.TLSSocket(socket); | ||
|
|
||
| // Direct import | ||
| const { TLSSocket } = require('node:tls'); | ||
| const socket2 = new TLSSocket(socket); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import tls from 'node:tls'; | ||
|
|
||
| // Using tls.SecurePair constructor | ||
| const socket = new tls.TLSSocket(socket); | ||
|
|
||
| // Direct import | ||
| import { TLSSocket } from 'node:tls'; | ||
| const socket2 = new TLSSocket(socket); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| const { TLSSocket } = require('node:tls'); | ||
| const { unrelated } = require('other-module'); | ||
|
|
||
| const socket = new TLSSocket(socket); | ||
|
|
||
| socket.on('error', (err) => { | ||
| console.error(err); | ||
| }); | ||
|
|
||
| // Obsolete properties |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| const { TLSSocket } = require('node:tls'); | ||
|
|
||
| // Completely arbitrary variable name | ||
| const socket = new TLSSocket(socket); | ||
|
|
||
| // Usage | ||
| socket.doSomething(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import tls from 'node:tls'; | ||
| import { TLSSocket } from 'node:tls'; | ||
|
|
||
| // Case 1: Via namespace | ||
| const socket1 = new tls.TLSSocket(socket); | ||
|
|
||
| // Case 2: Direct | ||
| const socket2 = new TLSSocket(socket); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| const tls = require('node:tls'); | ||
|
|
||
| const socket = new tls.TLSSocket(socket); | ||
| const mySocket = new tls.TLSSocket(socket); | ||
| const secureSocketInstance = new tls.TLSSocket(socket); | ||
|
|
||
| // Specific cleanup for each variable |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| const tls = require('node:tls'); | ||
|
|
||
| class ConnectionManager { | ||
| constructor() { | ||
| if (true) { | ||
| const socket = new tls.TLSSocket(socket); | ||
| this.init(socket); | ||
|
|
||
| if (socket.cleartext) { | ||
| console.log("cleaning"); | ||
| } | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.