diff --git a/Cargo.lock b/Cargo.lock
index 7c8c06a133..1a2c855b21 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2403,6 +2403,7 @@ dependencies = [
"deno_permissions",
"deno_process",
"deno_subprocess_windows",
+ "deno_tls",
"deno_whoami",
"der",
"digest",
@@ -2442,6 +2443,7 @@ dependencies = [
"ripemd",
"rsa",
"rusqlite",
+ "rustls-tokio-stream",
"scrypt",
"sec1",
"serde",
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml
index 14767d8e0a..c1acd65d88 100644
--- a/ext/node/Cargo.toml
+++ b/ext/node/Cargo.toml
@@ -40,6 +40,7 @@ deno_package_json.workspace = true
deno_path_util.workspace = true
deno_permissions.workspace = true
deno_process.workspace = true
+deno_tls.workspace = true
deno_whoami.workspace = true
der = { workspace = true, features = ["derive"] }
digest = { workspace = true, features = ["core-api", "std"] }
@@ -78,6 +79,7 @@ rand.workspace = true
ripemd = { workspace = true, features = ["oid"] }
rsa.workspace = true
rusqlite.workspace = true
+rustls-tokio-stream.workspace = true
scrypt.workspace = true
sec1.workspace = true
serde.workspace = true
diff --git a/ext/node/clippy.toml b/ext/node/clippy.toml
index 49e76c4a5a..58b1e31071 100644
--- a/ext/node/clippy.toml
+++ b/ext/node/clippy.toml
@@ -28,6 +28,3 @@ disallowed-methods = [
{ path = "std::fs::symlink_metadata", reason = "File system operations should be done using FileSystem trait" },
{ path = "std::fs::write", reason = "File system operations should be done using FileSystem trait" },
]
-disallowed-types = [
- { path = "std::sync::Arc", reason = "use deno_fs::sync::MaybeArc instead" },
-]
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index b9dfd690bb..23876e812a 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -460,6 +460,8 @@ deno_core::extension!(deno_node,
ops::tls::op_get_root_certificates,
ops::tls::op_tls_peer_certificate,
ops::tls::op_tls_canonicalize_ipv4_address,
+ ops::tls::op_node_tls_start,
+ ops::tls::op_node_tls_handshake,
ops::inspector::op_inspector_open
,
ops::inspector::op_inspector_close,
ops::inspector::op_inspector_url,
diff --git a/ext/node/ops/tls.rs b/ext/node/ops/tls.rs
index 8569e39a63..db1be31518 100644
--- a/ext/node/ops/tls.rs
+++ b/ext/node/ops/tls.rs
@@ -1,9 +1,46 @@
// Copyright 2018-2025 the Deno authors. MIT license.
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::collections::VecDeque;
+use std::future::Future;
+use std::io::Error;
+use std::io::ErrorKind;
+use std::num::NonZeroUsize;
+use std::pin::Pin;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::sync::Mutex;
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::Ordering;
+use std::task::Context;
+use std::task::Poll;
+
use base64::Engine;
+use bytes::Bytes;
+use deno_core::AsyncRefCell;
+use deno_core::AsyncResult;
use deno_core::OpState;
+use deno_core::RcRef;
+use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::op2;
+use deno_net::DefaultTlsOptions;
+use deno_net::UnsafelyIgnoreCertificateErrors;
+use deno_net::ops::NetError;
+use deno_net::ops::TlsHandshakeInfo;
use deno_net::ops_tls::TlsStreamResource;
+use deno_tls::SocketUse;
+use deno_tls::TlsClientConfigOptions;
+use deno_tls::TlsKeys;
+use deno_tls::TlsKeysHolder;
+use deno_tls::create_client_config;
+use deno_tls::rustls::ClientConnection;
+use deno_tls::rustls::pki_types::ServerName;
+use rustls_tokio_stream::TlsStream;
+use rustls_tokio_stream::TlsStreamRead;
+use rustls_tokio_stream::TlsStreamWrite;
+use rustls_tokio_stream::UnderlyingStream;
+use serde::Deserialize;
use webpki_root_certs;
use super::crypto::x509::Certificate;
@@ -68,3 +105,472 @@ pub fn op_tls_canonicalize_ipv4_address(
Some(canonical_ip)
}
+
+struct ReadableFuture<'a> {
+ socket: &'a JSStreamSocket,
+}
+
+impl<'a> Future for ReadableFuture<'a> {
+ type Output = std::io::Result<()>;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ self.socket.poll_read_ready(cx)
+ }
+}
+
+struct WritableFuture<'a> {
+ socket: &'a JSStreamSocket,
+}
+
+impl<'a> Future for WritableFuture<'a> {
+ type Output = std::io::Result<()>;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ self.socket.poll_write_ready(cx)
+ }
+}
+
+#[derive(Debug)]
+pub struct JSStreamSocket {
+ readable: Arc>>,
+ writable: tokio::sync::mpsc::Sender,
+ read_buffer: Arc>>,
+ closed: AtomicBool,
+}
+
+impl JSStreamSocket {
+ pub fn new(
+ readable: tokio::sync::mpsc::Receiver,
+ writable: tokio::sync::mpsc::Sender,
+ ) -> Self {
+ Self {
+ readable: Arc::new(Mutex::new(readable)),
+ writable,
+ read_buffer: Arc::new(Mutex::new(VecDeque::new())),
+ closed: AtomicBool::new(false),
+ }
+ }
+}
+
+impl UnderlyingStream for JSStreamSocket {
+ type StdType = ();
+
+ fn poll_read_ready(
+ &self,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll> {
+ // Check if we have buffered data
+ if let Ok(buffer) = self.read_buffer.lock()
+ && !buffer.is_empty()
+ {
+ return Poll::Ready(Ok(()));
+ }
+
+ if self.closed.load(Ordering::Relaxed) {
+ return Poll::Ready(Err(Error::new(
+ ErrorKind::UnexpectedEof,
+ "Stream closed",
+ )));
+ }
+
+ // Try to poll for data without consuming it
+ if let Ok(mut receiver) = self.readable.lock() {
+ match receiver.poll_recv(cx) {
+ Poll::Ready(Some(data)) => {
+ // Store the data in buffer for try_read
+ if let Ok(mut buffer) = self.read_buffer.lock() {
+ buffer.push_back(data);
+ }
+ Poll::Ready(Ok(()))
+ }
+ Poll::Ready(None) => {
+ // Channel closed
+ self.closed.store(true, Ordering::Relaxed);
+ Poll::Ready(Err(Error::new(
+ ErrorKind::UnexpectedEof,
+ "Channel closed",
+ )))
+ }
+ Poll::Pending => Poll::Pending,
+ }
+ } else {
+ panic!("Failed to acquire lock")
+ }
+ }
+
+ fn poll_write_ready(
+ &self,
+ _cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll> {
+ if self.closed.load(Ordering::Relaxed) {
+ return Poll::Ready(Err(Error::new(
+ ErrorKind::BrokenPipe,
+ "Stream closed",
+ )));
+ }
+
+ // For bounded sender, check if channel is ready
+ if self.writable.is_closed() {
+ self.closed.store(true, Ordering::Relaxed);
+ Poll::Ready(Err(Error::new(ErrorKind::BrokenPipe, "Channel closed")))
+ } else {
+ Poll::Ready(Ok(()))
+ }
+ }
+
+ fn try_read(&self, buf: &mut [u8]) -> std::io::Result {
+ if self.closed.load(Ordering::Relaxed) {
+ return Err(Error::new(ErrorKind::UnexpectedEof, "Stream closed"));
+ }
+
+ // Check if we have buffered data first
+ if let Ok(mut buffer) = self.read_buffer.lock()
+ && let Some(data) = buffer.pop_front()
+ {
+ let len = std::cmp::min(buf.len(), data.len());
+ buf[..len].copy_from_slice(&data[..len]);
+
+ // If there's leftover data, put it back in the buffer
+ if data.len() > len {
+ buffer.push_front(data.slice(len..));
+ }
+
+ return Ok(len);
+ }
+
+ // Try to read from channel non-blocking
+ if let Ok(mut receiver) = self.readable.lock() {
+ match receiver.try_recv() {
+ Ok(data) => {
+ let len = std::cmp::min(buf.len(), data.len());
+ buf[..len].copy_from_slice(&data[..len]);
+
+ // If there's leftover data, store it in buffer
+ if data.len() > len
+ && let Ok(mut buffer) = self.read_buffer.lock()
+ {
+ buffer.push_front(data.slice(len..));
+ }
+
+ Ok(len)
+ }
+ Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
+ Err(Error::new(ErrorKind::WouldBlock, "No data available"))
+ }
+ Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
+ self.closed.store(true, Ordering::Relaxed);
+ Err(Error::new(ErrorKind::UnexpectedEof, "Channel closed"))
+ }
+ }
+ } else {
+ Err(Error::other("Failed to acquire lock"))
+ }
+ }
+
+ fn try_write(&self, buf: &[u8]) -> std::io::Result {
+ if self.closed.load(Ordering::Relaxed) {
+ return Err(Error::new(ErrorKind::BrokenPipe, "Stream closed"));
+ }
+
+ if self.writable.is_closed() {
+ self.closed.store(true, Ordering::Relaxed);
+ return Err(Error::new(ErrorKind::BrokenPipe, "Channel closed"));
+ }
+
+ let data = Bytes::copy_from_slice(buf);
+ match self.writable.try_send(data) {
+ Ok(()) => Ok(buf.len()),
+ Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
+ Err(Error::new(ErrorKind::WouldBlock, "Channel full"))
+ }
+ Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
+ self.closed.store(true, Ordering::Relaxed);
+ Err(Error::new(ErrorKind::BrokenPipe, "Channel closed"))
+ }
+ }
+ }
+
+ fn readable(&self) -> impl Future