Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-jupyter-server for 
openSUSE:Factory checked in at 2026-06-30 15:13:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jupyter-server (Old)
 and      /work/SRC/openSUSE:Factory/.python-jupyter-server.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jupyter-server"

Tue Jun 30 15:13:35 2026 rev:48 rq:1362580 version:2.20.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-jupyter-server/python-jupyter-server.changes  
    2026-06-08 14:28:54.385984049 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-jupyter-server.new.11887/python-jupyter-server.changes
   2026-06-30 15:13:53.864984712 +0200
@@ -1,0 +2,23 @@
+Tue Jun 30 06:53:27 UTC 2026 - Daniel Garcia <[email protected]>
+
+- Update to 2.20.0 (CVE-2026-44727, bsc#1269630):
+  # Security fixes
+  - CVE-2026-44727 GHSA-fcw5-x6j4-ccmp
+  # Enhancements made
+  - Fix confusing terminal output when using ServerApp.ip=0.0.0.0
+    #1643
+  - Add a toggle to enable curve encryption for all kernels that
+    support it #1638
+  # Bugs fixed
+  - Grab the port from bind_sockets in case its different #1651
+  # Maintenance and upkeep improvements
+  - Fix test_authorizer having a spurious comma in params #1664
+  - Add a reminder to merge GHSA before release #1659
+  - Exclude problematic pywinpty 3.0.4 version #1658
+  - ci: explicitly pass base-setup inputs to fix strict validation
+    failures #1626
+  # Documentation improvements
+  - Align docs for curve encryption with latest JEP version #1660
+  - Remove PGP key from docs #1653
+
+-------------------------------------------------------------------

Old:
----
  jupyter_server-2.19.0.tar.gz

New:
----
  jupyter_server-2.20.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jupyter-server.spec ++++++
--- /var/tmp/diff_new_pack.Zxlqwo/_old  2026-06-30 15:13:55.165028732 +0200
+++ /var/tmp/diff_new_pack.Zxlqwo/_new  2026-06-30 15:13:55.177029139 +0200
@@ -33,7 +33,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-jupyter-server%{psuffix}
-Version:        2.19.0
+Version:        2.20.0
 Release:        0
 Summary:        The backend to Jupyter web applications
 License:        BSD-3-Clause

++++++ jupyter_server-2.19.0.tar.gz -> jupyter_server-2.20.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/.github/workflows/prep-release.yml 
new/jupyter_server-2.20.0/.github/workflows/prep-release.yml
--- old/jupyter_server-2.19.0/.github/workflows/prep-release.yml        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/.github/workflows/prep-release.yml        
2020-02-02 01:00:00.000000000 +0100
@@ -23,6 +23,10 @@
         description: "Use PRs with activity since the last stable git tag"
         required: false
         type: boolean
+      security_reminder:
+        description: "Recommended: check with a security maintainer if a patch 
is waiting to be merged. 
https://github.com/jupyter-server/jupyter_server/security/advisories?state=draft";
+        required: false
+        type: boolean
 jobs:
   prep_release:
     runs-on: ubuntu-latest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/.github/workflows/python-tests.yml 
new/jupyter_server-2.20.0/.github/workflows/python-tests.yml
--- old/jupyter_server-2.19.0/.github/workflows/python-tests.yml        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/.github/workflows/python-tests.yml        
2020-02-02 01:00:00.000000000 +0100
@@ -29,6 +29,10 @@
         uses: actions/checkout@v6
       - name: Base Setup
         uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
+          python_version: ${{ matrix.python-version }}
       - name: Install nbconvert dependencies on Linux
         if: startsWith(runner.os, 'Linux')
         run: |
@@ -49,6 +53,9 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - name: Install Dependencies
         run: |
           sudo apt-get update
@@ -70,6 +77,9 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - name: Run Linters
         run: |
           pipx run pre-commit run --all-files
@@ -86,6 +96,9 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - name: Install the Python dependencies for the examples
         run: |
           pip install -e ".[test]"
@@ -103,6 +116,7 @@
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
         with:
           dependency_type: minimum
+          python_package_manager: pip
       - name: Run the unit tests
         run: |
           hatch -vv run test:nowarn || hatch -v run test:nowarn --lf
@@ -116,6 +130,7 @@
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
         with:
           dependency_type: pre
+          python_package_manager: pip
           # pyO3 dependencies can't build on dev python
           python_version: "3.14"
       - name: Run the tests
@@ -129,6 +144,9 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - name: Create NFS file system
         run: |
           sudo apt-get install -y nfs-kernel-server
@@ -147,6 +165,9 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - uses: jupyterlab/maintainer-tools/.github/actions/make-sdist@v1
 
   test_sdist:
@@ -156,6 +177,9 @@
     timeout-minutes: 20
     steps:
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
         with:
           package_spec: -vv .
@@ -168,6 +192,9 @@
         uses: actions/checkout@v6
       - name: Base Setup
         uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - name: Install Dependencies
         run: |
           pip install -e .
@@ -183,6 +210,9 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
       - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
         with:
           ignore_links: "https://www.npmjs.com";
@@ -197,6 +227,10 @@
     steps:
       - uses: actions/checkout@v6
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+        with:
+          python_package_manager: pip
+          dependency_type: standard
+          python_version: ${{ matrix.python-version }}
       - name: Run the tests
         run: hatch -v run cov:integration
       - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/CHANGELOG.md 
new/jupyter_server-2.20.0/CHANGELOG.md
--- old/jupyter_server-2.19.0/CHANGELOG.md      2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.20.0/CHANGELOG.md      2020-02-02 01:00:00.000000000 
+0100
@@ -4,6 +4,42 @@
 
 <!-- <START NEW CHANGELOG ENTRY> -->
 
+## 2.20.0
+
+([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.19.0...333e700119ee0bcc0b5fcd4c158213d7c275c778))
+
+### Enhancements made
+
+- Fix confusing terminal output when using ServerApp.ip=0.0.0.0 
[#1643](https://github.com/jupyter-server/jupyter_server/pull/1643) 
([@Yann-P](https://github.com/Yann-P), [@minrk](https://github.com/minrk))
+- Add a toggle to enable curve encryption for all kernels that support it 
[#1638](https://github.com/jupyter-server/jupyter_server/pull/1638) 
([@krassowski](https://github.com/krassowski), 
[@Carreau](https://github.com/Carreau), 
[@ianthomas23](https://github.com/ianthomas23), 
[@minrk](https://github.com/minrk))
+
+### Bugs fixed
+
+- Grab the port from `bind_sockets` in case its different 
[#1651](https://github.com/jupyter-server/jupyter_server/pull/1651) 
([@choldgraf](https://github.com/choldgraf), 
[@krassowski](https://github.com/krassowski))
+
+### Maintenance and upkeep improvements
+
+- Fix `test_authorizer` having a spurious comma in params 
[#1664](https://github.com/jupyter-server/jupyter_server/pull/1664) 
([@krassowski](https://github.com/krassowski))
+- Add a reminder to merge GHSA before release 
[#1659](https://github.com/jupyter-server/jupyter_server/pull/1659) 
([@Yann-P](https://github.com/Yann-P), [@Carreau](https://github.com/Carreau))
+- Exclude problematic `pywinpty` 3.0.4 version 
[#1658](https://github.com/jupyter-server/jupyter_server/pull/1658) 
([@krassowski](https://github.com/krassowski))
+- ci: explicitly pass base-setup inputs to fix strict validation failures 
[#1626](https://github.com/jupyter-server/jupyter_server/pull/1626) 
([@Carreau](https://github.com/Carreau), [@Copilot](https://github.com/Copilot))
+
+### Documentation improvements
+
+- Align docs for curve encryption with latest JEP version 
[#1660](https://github.com/jupyter-server/jupyter_server/pull/1660) 
([@krassowski](https://github.com/krassowski), 
[@Carreau](https://github.com/Carreau))
+- Remove PGP key from docs 
[#1653](https://github.com/jupyter-server/jupyter_server/pull/1653) 
([@Yann-P](https://github.com/Yann-P), 
[@krassowski](https://github.com/krassowski))
+
+### Contributors to this release
+
+The following people contributed discussions, new ideas, code and 
documentation contributions, and review.
+See [our definition of 
contributors](https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports).
+
+([GitHub contributors page for this 
release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2026-05-29&to=2026-06-17&type=c))
+
+@Carreau 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @choldgraf 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Acholdgraf+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @Copilot 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACopilot+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @ianthomas23 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aianthomas23+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @minrk 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @Yann-P ([activity](https://github.com/search?q
 
=repo%3Ajupyter-server%2Fjupyter_server+involves%3AYann-P+updated%3A2026-05-29..2026-06-17&type=Issues))
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
 ## 2.19.0
 
 ([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.18.2...664e2255c71efe963f397b9f803dbcf503b5a920))
@@ -36,8 +72,6 @@
 
 @Carreau 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @ianthomas23 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aianthomas23+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @minrk 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @MUFFANUJ 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AMUFFANUJ+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @terminalchai 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aterminalchai+updated%3A2026-05-06..2026-05-29&type=Issues))
 | @Zsailer ([activity](https://github.com
 
/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AZsailer+updated%3A2026-05-06..2026-05-29&type=Issues))
 
-<!-- <END NEW CHANGELOG ENTRY> -->
-
 ## 2.18.2
 
 ([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.18.1...0d829f2c35a481c3b24ecbe1e25a6f79954e88f2))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/PKG-INFO 
new/jupyter_server-2.20.0/PKG-INFO
--- old/jupyter_server-2.19.0/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: jupyter_server
-Version: 2.19.0
+Version: 2.20.0
 Summary: The backend—i.e. core services, APIs, and REST endpoints—to Jupyter 
web applications.
 Project-URL: Homepage, https://jupyter-server.readthedocs.io
 Project-URL: Documentation, https://jupyter-server.readthedocs.io
@@ -63,7 +63,7 @@
 Requires-Dist: overrides>=5.0; python_version < '3.12'
 Requires-Dist: packaging>=22.0
 Requires-Dist: prometheus-client>=0.9
-Requires-Dist: pywinpty>=2.0.1; os_name == 'nt'
+Requires-Dist: pywinpty!=3.0.4,>=2.0.1; os_name == 'nt'
 Requires-Dist: pyzmq>=24
 Requires-Dist: send2trash>=1.8.2
 Requires-Dist: terminado>=0.8.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/RELEASE.md 
new/jupyter_server-2.20.0/RELEASE.md
--- old/jupyter_server-2.19.0/RELEASE.md        2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.20.0/RELEASE.md        2020-02-02 01:00:00.000000000 
+0100
@@ -7,6 +7,8 @@
 Note that we must use manual versions since Jupyter Releaser does not
 yet support "next" or "patch" when dev versions are used.
 
+Note about Security Advisories: some patches are developed in private hidden 
forks that only the [security team](https://github.com/jupyter/security) can 
access; please check with them that no patch is waiting be be merged before 
releasing.
+
 ## Manual Release
 
 To create a manual release, perform the following steps:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/docs/source/operators/ipython_security.asc 
new/jupyter_server-2.20.0/docs/source/operators/ipython_security.asc
--- old/jupyter_server-2.19.0/docs/source/operators/ipython_security.asc        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/docs/source/operators/ipython_security.asc        
1970-01-01 01:00:00.000000000 +0100
@@ -1,52 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: GnuPG v2.0.22 (GNU/Linux)
-
-mQINBFMx2LoBEAC9xU8JiKI1VlCJ4PT9zqhU5nChQZ06/bj1BBftiMJG07fdGVO0
-ibOn4TrCoRYaeRlet0UpHzxT4zDa5h3/usJaJNTSRwtWePw2o7Lik8J+F3LionRf
-8Jz81WpJ+81Klg4UWKErXjBHsu/50aoQm6ZNYG4S2nwOmMVEC4nc44IAA0bb+6kW
-saFKKzEDsASGyuvyutdyUHiCfvvh5GOC2h9mXYvl4FaMW7K+d2UgCYERcXDNy7C1
-Bw+uepQ9ELKdG4ZpvonO6BNr1BWLln3wk93AQfD5qhfsYRJIyj0hJlaRLtBU3i6c
-xs+gQNF4mPmybpPSGuOyUr4FYC7NfoG7IUMLj+DYa6d8LcMJO+9px4IbdhQvzGtC
-qz5av1TX7/+gnS4L8C9i1g8xgI+MtvogngPmPY4repOlK6y3l/WtxUPkGkyYkn3s
-RzYyE/GJgTwuxFXzMQs91s+/iELFQq/QwmEJf+g/QYfSAuM+lVGajEDNBYVAQkxf
-gau4s8Gm0GzTZmINilk+7TxpXtKbFc/Yr4A/fMIHmaQ7KmJB84zKwONsQdVv7Jjj
-0dpwu8EIQdHxX3k7/Q+KKubEivgoSkVwuoQTG15X9xrOsDZNwfOVQh+JKazPvJtd
-SNfep96r9t/8gnXv9JI95CGCQ8lNhXBUSBM3BDPTbudc4b6lFUyMXN0mKQARAQAB
-tCxJUHl0aG9uIFNlY3VyaXR5IFRlYW0gPHNlY3VyaXR5QGlweXRob24ub3JnPokC
-OAQTAQIAIgUCUzHYugIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQEwJc
-LcmZYkjuXg//R/t6nMNQmf9W1h52IVfUbRAVmvZ5d063hQHKV2dssxtnA2dRm/x5
-JZu8Wz7ZrEZpyqwRJO14sxN1/lC3v+zs9XzYXr2lBTZuKCPIBypYVGIynCuWJBQJ
-rWnfG4+u1RHahnjqlTWTY1C/le6v7SjAvCb6GbdA6k4ZL2EJjQlRaHDmzw3rV/+l
-LLx6/tYzIsotuflm/bFumyOMmpQQpJjnCkWIVjnRICZvuAn97jLgtTI0+0Rzf4Zb
-k2BwmHwDRqWCTTcRI9QvTl8AzjW+dNImN22TpGOBPfYj8BCZ9twrpKUbf+jNqJ1K
-THQzFtpdJ6SzqiFVm74xW4TKqCLkbCQ/HtVjTGMGGz/y7KTtaLpGutQ6XE8SSy6P
-EffSb5u+kKlQOWaH7Mc3B0yAojz6T3j5RSI8ts6pFi6pZhDg9hBfPK2dT0v/7Mkv
-E1Z7q2IdjZnhhtGWjDAMtDDn2NbY2wuGoa5jAWAR0WvIbEZ3kOxuLE5/ZOG1FyYm
-noJRliBz7038nT92EoD5g1pdzuxgXtGCpYyyjRZwaLmmi4CvA+oThKmnqWNY5lyY
-ricdNHDiyEXK0YafJL1oZgM86MSb0jKJMp5U11nUkUGzkroFfpGDmzBwAzEPgeiF
-40+qgsKB9lqwb3G7PxvfSi3XwxfXgpm1cTyEaPSzsVzve3d1xeqb7Yq5Ag0EUzHY
-ugEQALQ5FtLdNoxTxMsgvrRr1ejLiUeRNUfXtN1TYttOfvAhfBVnszjtkpIW8DCB
-JF/bA7ETiH8OYYn/Fm6MPI5H64IHEncpzxjf57jgpXd9CA9U2OMk/P1nve5zYchP
-QmP2fJxeAWr0aRH0Mse5JS5nCkh8Xv4nAjsBYeLTJEVOb1gPQFXOiFcVp3gaKAzX
-GWOZ/mtG/uaNsabH/3TkcQQEgJefd11DWgMB7575GU+eME7c6hn3FPITA5TC5HUX
-azvjv/PsWGTTVAJluJ3fUDvhpbGwYOh1uV0rB68lPpqVIro18IIJhNDnccM/xqko
-4fpJdokdg4L1wih+B04OEXnwgjWG8OIphR/oL/+M37VV2U7Om/GE6LGefaYccC9c
-tIaacRQJmZpG/8RsimFIY2wJ07z8xYBITmhMmOt0bLBv0mU0ym5KH9Dnru1m9QDO
-AHwcKrDgL85f9MCn+YYw0d1lYxjOXjf+moaeW3izXCJ5brM+MqVtixY6aos3YO29
-J7SzQ4aEDv3h/oKdDfZny21jcVPQxGDui8sqaZCi8usCcyqWsKvFHcr6vkwaufcm
-3Knr2HKVotOUF5CDZybopIz1sJvY/5Dx9yfRmtivJtglrxoDKsLi1rQTlEQcFhCS
-ACjf7txLtv03vWHxmp4YKQFkkOlbyhIcvfPVLTvqGerdT2FHABEBAAGJAh8EGAEC
-AAkFAlMx2LoCGwwACgkQEwJcLcmZYkgK0BAAny0YUugpZldiHzYNf8I6p2OpiDWv
-ZHaguTTPg2LJSKaTd+5UHZwRFIWjcSiFu+qTGLNtZAdcr0D5f991CPvyDSLYgOwb
-Jm2p3GM2KxfECWzFbB/n/PjbZ5iky3+5sPlOdBR4TkfG4fcu5GwUgCkVe5u3USAk
-C6W5lpeaspDz39HAPRSIOFEX70+xV+6FZ17B7nixFGN+giTpGYOEdGFxtUNmHmf+
-waJoPECyImDwJvmlMTeP9jfahlB6Pzaxt6TBZYHetI/JR9FU69EmA+XfCSGt5S+0
-Eoc330gpsSzo2VlxwRCVNrcuKmG7PsFFANok05ssFq1/Djv5rJ++3lYb88b8HSP2
-3pQJPrM7cQNU8iPku9yLXkY5qsoZOH+3yAia554Dgc8WBhp6fWh58R0dIONQxbbo
-apNdwvlI8hKFB7TiUL6PNShE1yL+XD201iNkGAJXbLMIC1ImGLirUfU267A3Cop5
-hoGs179HGBcyj/sKA3uUIFdNtP+NndaP3v4iYhCitdVCvBJMm6K3tW88qkyRGzOk
-4PW422oyWKwbAPeMk5PubvEFuFAIoBAFn1zecrcOg85RzRnEeXaiemmmH8GOe1Xu
-Kh+7h8XXyG6RPFy8tCcLOTk+miTqX+4VWy+kVqoS2cQ5IV8WsJ3S7aeIy0H89Z8n
-5vmLc+Ibz+eT+rM=
-=XVDe
------END PGP PUBLIC KEY BLOCK-----
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/docs/source/operators/security.rst 
new/jupyter_server-2.20.0/docs/source/operators/security.rst
--- old/jupyter_server-2.19.0/docs/source/operators/security.rst        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/docs/source/operators/security.rst        
2020-02-02 01:00:00.000000000 +0100
@@ -366,6 +366,107 @@
 ``@authenticated`` decorator for authentication (from ``tornado.web``).
 
 
+Kernel transport encryption
+---------------------------
+
+.. versionadded:: 2.20
+
+By default, ZMQ sockets used to communicate with kernels (shell, IOPub, stdin,
+control, heartbeat) are bound to TCP ports with no transport-level encryption.
+Any process on the same host that can reach those ports can connect and read
+messages, including all IOPub output.
+
+`CurveZMQ <https://rfc.zeromq.org/spec/26/>`__ adds elliptic-curve
+encryption and authentication directly at the ZMQ transport layer.
+When enabled, the ``KernelManager`` generates a keypair, writes the keys into
+the kernel's connection file, and configures all sockets as a CurveZMQ server.
+Clients must present the correct server public key to connect; unauthenticated
+connections are silently dropped before any data is delivered.
+
+.. note::
+
+    CurveZMQ is only available when pyzmq was compiled against a libzmq that
+    includes libsodium. You can verify this with::
+
+        python -c "import zmq; print(zmq.has('curve'))"
+
+    If this prints ``False``, the ``transport_encryption`` setting has no
+    effect and attempts to set it to ``'auto'`` or ``'required'`` will raise
+    a configuration error.
+
+The ``transport_encryption`` setting
+*************************************
+
+Transport encryption is controlled by the 
``KernelManager.transport_encryption``
+traitlet, which accepts three values:
+
+``'disabled'`` (default)
+    No CurveZMQ keys are generated. All kernel sockets are unencrypted.
+
+``'auto'``
+    Keys are generated **only when the kernelspec declares support** via
+    ``metadata.supported_encryption: ['curve']``. Kernelspecs that do not
+    declare this field are started without encryption, so the setting is safe
+    to enable globally without breaking existing kernels.
+
+``'required'``
+    Keys are always generated. Startup fails with a ``RuntimeError`` if the
+    kernelspec does not declare ``metadata.supported_encryption: ['curve']``,
+    so kernels that have not been updated to handle the connection-file keys
+    are never started unencrypted.
+
+.. note::
+
+    ``transport_encryption`` applies to both the ``tcp`` and ``ipc``
+    transports. IPC sockets also rely on filesystem permissions for access
+    control, but CurveZMQ can be layered on top for defense in depth.
+
+To enable encryption globally for all kernels that support it, add the
+following to your :file:`jupyter_server_config.py`:
+
+.. sourcecode:: python
+
+    c.KernelManager.transport_encryption = "auto"
+
+To enforce encryption and prevent unencrypted kernels from starting:
+
+.. sourcecode:: python
+
+    c.KernelManager.transport_encryption = "required"
+
+Kernelspec requirements
+***********************
+
+A kernel must declare CurveZMQ support in its :file:`kernel.json` before the
+``KernelManager`` will provision keys for it:
+
+.. sourcecode:: json
+
+    {
+        "argv": ["python", "-m", "ipykernel_launcher", "-f", 
"{connection_file}"],
+        "display_name": "Python 3",
+        "language": "python",
+        "metadata": {
+            "supported_encryption": ["curve"]
+        }
+    }
+
+When ``transport_encryption`` is ``'auto'``, kernelspecs without this field are
+started normally without encryption. When it is ``'required'``, their startup
+is refused.
+
+The kernel itself must read ``curve_publickey`` and ``curve_secretkey`` from
+the connection file and apply them to its ZMQ sockets. Kernels based on
+ipykernel 7.3 do this automatically when the fields are present.
+
+.. note::
+
+    When updating a previously installed kernel to a version that supports 
encryption
+    you may need to re-install the kernelspec or manually add the 
``supported_encryption``
+    metadata field. If you subsequently decide to downgrade, you will need to
+    remove this field as otherwise the kernel will silently fail to connect.
+
+
 Security in notebook documents
 ==============================
 
@@ -453,6 +554,22 @@
 These two methods simply load the notebook, compute a new signature, and add
 that signature to the user's database.
 
+Content-Security-Policy for nbconvert
+--------------------------------------
+
+When notebooks are rendered via nbconvert (``/nbconvert/`` endpoints),
+the server adds a ``sandbox allow-scripts`` directive to the
+``Content-Security-Policy`` header by default. This confines any
+JavaScript in the rendered output to a unique origin, preventing it
+from interacting with the Jupyter server.
+
+This behavior is controlled by 
:attr:`~jupyter_server.serverapp.ServerApp.nbconvert_csp_sandbox`:
+
+.. sourcecode:: python
+
+    # jupyter_server_config.py
+    c.ServerApp.nbconvert_csp_sandbox = True  # default
+
 Reporting security issues
 -------------------------
 
@@ -460,9 +577,6 @@
 code to properly implement the model described here, or a failure of the
 model itself, please report it to [email protected].
 
-If you prefer to encrypt your security reports,
-you can use :download:`this PGP public key <ipython_security.asc>`.
-
 Affected use cases
 ------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/jupyter_server/_version.py 
new/jupyter_server-2.20.0/jupyter_server/_version.py
--- old/jupyter_server-2.19.0/jupyter_server/_version.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/jupyter_server/_version.py        2020-02-02 
01:00:00.000000000 +0100
@@ -6,7 +6,7 @@
 import re
 
 # Version string must appear intact for automatic versioning
-__version__ = "2.19.0"
+__version__ = "2.20.0"
 
 # Build up version_info tuple for backwards compatibility
 pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/jupyter_server/nbconvert/handlers.py 
new/jupyter_server-2.20.0/jupyter_server/nbconvert/handlers.py
--- old/jupyter_server-2.19.0/jupyter_server/nbconvert/handlers.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/jupyter_server/nbconvert/handlers.py      
2020-02-02 01:00:00.000000000 +0100
@@ -92,6 +92,14 @@
     auth_resource = AUTH_RESOURCE
     SUPPORTED_METHODS = ("GET",)
 
+    @property
+    def content_security_policy(self):
+        # In case we're serving HTML, confine any Javascript to a unique
+        # origin so it can't interact with the Jupyter server.
+        if self.settings.get("nbconvert_csp_sandbox", True):
+            return super().content_security_policy + "; sandbox allow-scripts"
+        return super().content_security_policy
+
     @web.authenticated
     @authorized
     async def get(self, format, path):
@@ -173,6 +181,14 @@
     SUPPORTED_METHODS = ("POST",)
     auth_resource = AUTH_RESOURCE
 
+    @property
+    def content_security_policy(self):
+        # In case we're serving HTML, confine any Javascript to a unique
+        # origin so it can't interact with the Jupyter server.
+        if self.settings.get("nbconvert_csp_sandbox", True):
+            return super().content_security_policy + "; sandbox allow-scripts"
+        return super().content_security_policy
+
     @web.authenticated
     @authorized
     async def post(self, format):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/jupyter_server/serverapp.py 
new/jupyter_server-2.20.0/jupyter_server/serverapp.py
--- old/jupyter_server-2.19.0/jupyter_server/serverapp.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/jupyter_server/serverapp.py       2020-02-02 
01:00:00.000000000 +0100
@@ -457,6 +457,7 @@
             "websocket_ping_timeout": websocket_ping_timeout,
             # handlers
             "extra_services": extra_services,
+            "nbconvert_csp_sandbox": jupyter_app.nbconvert_csp_sandbox,
             # Jupyter stuff
             "started": now,
             # place for extensions to register activity
@@ -1604,6 +1605,15 @@
         help="""If True, display controls to shut down the Jupyter server, 
such as menu items or buttons.""",
     )
 
+    nbconvert_csp_sandbox = Bool(
+        True,
+        config=True,
+        help=_i18n(
+            "If True, add a 'sandbox' directive to the Content-Security-Policy 
header for nbconvert-served pages, "
+            "confining any JavaScript to a unique origin so it cannot interact 
with the Jupyter server."
+        ),
+    )
+
     contents_manager_class = Type(
         default_value=AsyncLargeFileManager,
         klass=ContentsManager,
@@ -2341,23 +2351,27 @@
             resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
 
     def _get_urlparts(
-        self, path: str | None = None, include_token: bool = False
+        self,
+        path: str | None = None,
+        include_token: bool = False,
+        ip: str | None = None,
     ) -> urllib.parse.ParseResult:
         """Constructs a urllib named tuple, ParseResult,
         with default values set by server config.
         The returned tuple can be manipulated using the `_replace` method.
         """
+        if ip is None:
+            ip = self.ip
         if self.sock:
             scheme = "http+unix"
             netloc = urlencode_unix_socket_path(self.sock)
         else:
-            if not self.ip:
+            # Empty string means "unset", fallback to localhost so the url
+            # is valid e.g. in k8s where gethostname() != localhost #743
+            if not ip:
                 ip = "localhost"
-            # Handle nonexplicit hostname.
-            elif self.ip in ("0.0.0.0", "::"):  # noqa: S104
-                ip = "%s" % socket.gethostname()
             else:
-                ip = f"[{self.ip}]" if ":" in self.ip else self.ip
+                ip = f"[{ip}]" if ":" in ip else ip
             netloc = f"{ip}:{self.port}"
             scheme = "https" if self.certfile else "http"
         if not path:
@@ -2412,6 +2426,23 @@
         urlparts = self._get_urlparts(path=self.base_url)
         return urlparts.geturl()
 
+    @property
+    def connect_url(self) -> str:
+        """Human readable string with URLs for connecting to the running
+        Jupyter Server.
+
+        If `ip` is the wildcard address, add a text hint but keep
+        the machine hostname in the link as 0.0.0.0 is not connectable.
+        """
+        if self.ip not in ("0.0.0.0", "::"):  # noqa: S104
+            return self.display_url
+        public = self._get_urlparts(include_token=True, 
ip=socket.gethostname()).geturl()
+        hint = _i18n(
+            "The server is listening on all interfaces, "
+            "so any hostname or IP of this machine will work."
+        )
+        return f"{public}\n    {self.local_url}\n{hint}"
+
     def init_signal(self) -> None:
         """Initialize signal handlers."""
         if not sys.platform.startswith("win") and sys.stdin and 
sys.stdin.isatty():
@@ -2691,6 +2722,9 @@
         for port in random_ports(self.port, self.port_retries + 1):
             try:
                 sockets = bind_sockets(port, self.ip)
+                # When port=0 the OS assigns a free port, so we read it back 
here.
+                if port == 0:
+                    port = sockets[0].getsockname()[1]
                 for s in sockets:
                     s.close()
             except OSError as e:
@@ -3138,7 +3172,7 @@
                     message = [
                         "\n",
                         _i18n("To access the server, copy and paste one of 
these URLs:"),
-                        "    %s" % self.display_url,
+                        "    %s" % self.connect_url,
                     ]
                 else:
                     message = [
@@ -3150,7 +3184,7 @@
                         _i18n(
                             "Or copy and paste one of these URLs:",
                         ),
-                        "    %s" % self.display_url,
+                        "    %s" % self.connect_url,
                     ]
 
                 self.log.critical("\n".join(message))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/jupyter_server/services/kernels/kernelmanager.py 
new/jupyter_server-2.20.0/jupyter_server/services/kernels/kernelmanager.py
--- old/jupyter_server-2.19.0/jupyter_server/services/kernels/kernelmanager.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/jupyter_server/services/kernels/kernelmanager.py  
2020-02-02 01:00:00.000000000 +0100
@@ -37,6 +37,7 @@
 from traitlets import (
     Any,
     Bool,
+    CaselessStrEnum,
     Dict,
     Float,
     Instance,
@@ -67,6 +68,18 @@
 
     kernel_argv = List(Unicode())
 
+    transport_encryption = CaselessStrEnum(
+        ["disabled", "auto", "required"],
+        default_value="disabled",
+        config=True,
+        help=(
+            "Transport encryption policy for manager-provisioned CurveZMQ keys 
for all managed kernels. "
+            "'disabled' (default) does not provision Curve credentials, 'auto' 
provisions when the kernelspec "
+            "declares support, and 'required' enforces provisioning and fails 
kernel startup if encryption "
+            "cannot be applied."
+        ),
+    )
+
     root_dir = Unicode(config=True)
 
     _kernel_connections = Dict()
@@ -204,6 +217,13 @@
             os_path = os.path.dirname(os_path)
         return os_path
 
+    def _kernel_start_kwargs(self, **kwargs: t.Any) -> dict[str, t.Any]:
+        """Build kernel launch kwargs with server-level policy applied."""
+        launch_kwargs = dict(kwargs)
+        if self.transport_encryption != "disabled":
+            launch_kwargs["transport_encryption"] = self.transport_encryption
+        return launch_kwargs
+
     async def _remove_kernel_when_ready(self, kernel_id, kernel_awaitable):
         """Remove a kernel when it is ready."""
         await super()._remove_kernel_when_ready(kernel_id, kernel_awaitable)
@@ -213,7 +233,7 @@
     # TODO: DEC 2022: Revise the type-ignore once the signatures have been 
changed upstream
     # https://github.com/jupyter/jupyter_client/pull/905
     async def _async_start_kernel(  # type:ignore[override]
-        self, *, kernel_id: str | None = None, path: ApiPath | None = None, 
**kwargs: str
+        self, *, kernel_id: str | None = None, path: ApiPath | None = None, 
**kwargs: t.Any
     ) -> str:
         """Start a kernel for a session and return its kernel_id.
 
@@ -231,6 +251,7 @@
             an existing kernel is returned, but it may be checked in the 
future.
         """
         if kernel_id is None or kernel_id not in self:
+            kwargs = self._kernel_start_kwargs(**kwargs)
             if path is not None:
                 kwargs["cwd"] = self.cwd_for_path(path, env=kwargs.get("env", 
{}))
             if kernel_id is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/pyproject.toml 
new/jupyter_server-2.20.0/pyproject.toml
--- old/jupyter_server-2.19.0/pyproject.toml    2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.20.0/pyproject.toml    2020-02-02 01:00:00.000000000 
+0100
@@ -33,7 +33,11 @@
     "nbformat>=5.3.0",
     "packaging>=22.0",
     "prometheus_client>=0.9",
-    "pywinpty>=2.0.1;os_name=='nt'",
+    # pywinpty 3.0.4 ships broken Windows wheels: winpty.dll (amd64) and all 
native
+    # binaries (arm64) are no longer bundled, so the .pyd fails to load -> 
import winpty
+    # raises ImportError -> terminado falls back to `object` -> object.spawn 
AttributeError.
+    # See https://github.com/andfoy/pywinpty/issues/573 (regressed in 3.0.4, 
2026-06-10).
+    "pywinpty>=2.0.1,!=3.0.4;os_name=='nt'",
     "pyzmq>=24",
     "Send2Trash>=1.8.2",
     "terminado>=0.8.3",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/tests/auth/test_authorizer.py 
new/jupyter_server-2.20.0/tests/auth/test_authorizer.py
--- old/jupyter_server-2.19.0/tests/auth/test_authorizer.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/tests/auth/test_authorizer.py     2020-02-02 
01:00:00.000000000 +0100
@@ -246,7 +246,7 @@
 
 
 @pytest.mark.parametrize(
-    "jp_server_config,",
+    "jp_server_config",
     [
         {
             "ServerApp": {"authorizer_class": AsyncAuthorizerTest},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/tests/nbconvert/test_handlers.py 
new/jupyter_server-2.20.0/tests/nbconvert/test_handlers.py
--- old/jupyter_server-2.19.0/tests/nbconvert/test_handlers.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/tests/nbconvert/test_handlers.py  2020-02-02 
01:00:00.000000000 +0100
@@ -190,3 +190,38 @@
     r = await jp_fetch("nbconvert", "latex", method="POST", 
body=json.dumps(nbmodel))
     assert "application/zip" in r.headers["Content-Type"]
     assert ".zip" in r.headers["Content-Disposition"]
+
+
[email protected](
+    "jp_server_config,expected",
+    [
+        ({"ServerApp": {"nbconvert_csp_sandbox": True}}, "sandbox 
allow-scripts"),
+        ({"ServerApp": {"nbconvert_csp_sandbox": False}}, None),
+    ],
+)
+async def test_csp_file_handler(jp_fetch, jp_server_config, notebook, 
expected):
+    r = await jp_fetch("nbconvert", "html", "foo", "testnb.ipynb", 
method="GET")
+    csp = r.headers["Content-Security-Policy"]
+    if expected:
+        assert expected in csp
+    else:
+        assert "sandbox" not in csp
+
+
[email protected](
+    "jp_server_config,expected",
+    [
+        ({"ServerApp": {"nbconvert_csp_sandbox": True}}, "sandbox 
allow-scripts"),
+        ({"ServerApp": {"nbconvert_csp_sandbox": False}}, None),
+    ],
+)
+async def test_csp_post_handler(jp_fetch, jp_server_config, notebook, 
expected):
+    r = await jp_fetch("api/contents/foo/testnb.ipynb", method="GET")
+    nbmodel = json.loads(r.body.decode())
+
+    r = await jp_fetch("nbconvert", "html", method="POST", 
body=json.dumps(nbmodel))
+    csp = r.headers["Content-Security-Policy"]
+    if expected:
+        assert expected in csp
+    else:
+        assert "sandbox" not in csp
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/tests/services/kernels/test_config.py 
new/jupyter_server-2.20.0/tests/services/kernels/test_config.py
--- old/jupyter_server-2.19.0/tests/services/kernels/test_config.py     
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/tests/services/kernels/test_config.py     
2020-02-02 01:00:00.000000000 +0100
@@ -1,7 +1,10 @@
 import pytest
 from traitlets.config import Config
 
-from jupyter_server.services.kernels.kernelmanager import 
AsyncMappingKernelManager
+from jupyter_server.services.kernels.kernelmanager import (
+    AsyncMappingKernelManager,
+    MappingKernelManager,
+)
 
 
 @pytest.fixture
@@ -29,3 +32,21 @@
     ]
     with pytest.warns(FutureWarning, match="is not a subclass of 
'ServerKernelManager'"):
         jp_configurable_serverapp(argv=argv)
+
+
+def test_kernel_start_kwargs_transport_encryption_sets_flag():
+    km = MappingKernelManager(transport_encryption="auto")
+
+    launch_kwargs = km._kernel_start_kwargs(env={"EXISTING": "1"})
+
+    assert launch_kwargs["transport_encryption"] == "auto"
+    assert launch_kwargs["env"] == {"EXISTING": "1"}
+
+
+def test_kernel_start_kwargs_transport_encryption_required_sets_policy():
+    km = MappingKernelManager(transport_encryption="required")
+
+    launch_kwargs = km._kernel_start_kwargs(env={"EXISTING": "1"})
+
+    assert launch_kwargs["transport_encryption"] == "required"
+    assert launch_kwargs["env"] == {"EXISTING": "1"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.19.0/tests/services/kernels/test_transport_encryption.py 
new/jupyter_server-2.20.0/tests/services/kernels/test_transport_encryption.py
--- 
old/jupyter_server-2.19.0/tests/services/kernels/test_transport_encryption.py   
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/jupyter_server-2.20.0/tests/services/kernels/test_transport_encryption.py   
    2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,111 @@
+"""End-to-end tests for MappingKernelManager.transport_encryption."""
+
+import json
+import sys
+import warnings
+
+import jupyter_client
+import pytest
+import zmq
+from traitlets.config import Config
+
+TEST_TIMEOUT = 60
+
+pytestmark = [
+    pytest.mark.skipif(
+        jupyter_client._version.version_info < (8, 9),
+        reason="transport_encryption requires jupyter-client >= 8.9",
+    ),
+    pytest.mark.skipif(
+        not zmq.has("curve"),
+        reason="CurveZMQ not available in this environment (zmq.has('curve') 
is False)",
+    ),
+]
+
+
[email protected](autouse=True)
+def suppress_deprecation_warnings():
+    with warnings.catch_warnings():
+        warnings.filterwarnings(
+            "ignore",
+            message="The synchronous MappingKernelManager",
+            category=DeprecationWarning,
+        )
+        yield
+
+
[email protected]
+def non_curve_kernel_spec(jp_data_dir):
+    """Install a minimal kernel spec that does not declare curve encryption 
support."""
+    kernel_dir = jp_data_dir / "kernels" / "no_curve"
+    kernel_dir.mkdir(parents=True)
+    (kernel_dir / "kernel.json").write_text(
+        json.dumps(
+            {
+                "argv": [sys.executable, "-c", "pass"],
+                "display_name": "No Curve",
+                "language": "python",
+            }
+        )
+    )
+    return "no_curve"
+
+
[email protected](TEST_TIMEOUT)
+async def 
test_transport_encryption_disabled_no_curve_keys(jp_configurable_serverapp):
+    """Default 'disabled' policy never provisions curve keys, even for kernels 
that support them."""
+    app = jp_configurable_serverapp()
+    km = app.kernel_manager
+    kernel_id = await km.start_kernel()
+    try:
+        kernel = km.get_kernel(kernel_id)
+        info = kernel.get_connection_info()
+        assert "curve_publickey" not in info
+        assert "curve_secretkey" not in info
+    finally:
+        await km.shutdown_kernel(kernel_id, now=True)
+
+
[email protected](TEST_TIMEOUT)
[email protected]("policy", ["auto", "required"])
+async def 
test_transport_encryption_provisions_curve_keys(jp_configurable_serverapp, 
policy):
+    """'auto' and 'required' both provision curve keys for kernelspecs that 
declare curve support."""
+    config = Config({"MappingKernelManager": {"transport_encryption": policy}})
+    app = jp_configurable_serverapp(config=config)
+    km = app.kernel_manager
+    kernel_id = await km.start_kernel()
+    try:
+        kernel = km.get_kernel(kernel_id)
+        info = kernel.get_connection_info()
+        assert "curve_publickey" in info
+        assert "curve_secretkey" in info
+    finally:
+        await km.shutdown_kernel(kernel_id, now=True)
+
+
[email protected](TEST_TIMEOUT)
+async def test_transport_encryption_auto_skips_keys_for_non_curve_kernel(
+    non_curve_kernel_spec, jp_configurable_serverapp
+):
+    """'auto' silently skips key provisioning when the kernelspec lacks curve 
support metadata."""
+    config = Config({"MappingKernelManager": {"transport_encryption": "auto"}})
+    app = jp_configurable_serverapp(config=config)
+    km = app.kernel_manager
+    kernel_id = await km.start_kernel(kernel_name=non_curve_kernel_spec)
+    try:
+        kernel = km.get_kernel(kernel_id)
+        info = kernel.get_connection_info()
+        assert "curve_publickey" not in info
+        assert "curve_secretkey" not in info
+    finally:
+        await km.shutdown_kernel(kernel_id, now=True)
+
+
+async def test_transport_encryption_required_raises_for_non_curve_kernel(
+    non_curve_kernel_spec, jp_configurable_serverapp
+):
+    """'required' raises RuntimeError at startup when the kernelspec lacks 
curve support metadata."""
+    config = Config({"MappingKernelManager": {"transport_encryption": 
"required"}})
+    app = jp_configurable_serverapp(config=config)
+    with pytest.raises(RuntimeError, match=r"metadata\.supported_encryption"):
+        await 
app.kernel_manager.start_kernel(kernel_name=non_curve_kernel_spec)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.19.0/tests/test_serverapp.py 
new/jupyter_server-2.20.0/tests/test_serverapp.py
--- old/jupyter_server-2.19.0/tests/test_serverapp.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.20.0/tests/test_serverapp.py   2020-02-02 
01:00:00.000000000 +0100
@@ -317,11 +317,26 @@
             "http+unix://%2Ftmp%2Fjp-test.sock/",
         ),
         (
+            # https://github.com/jupyter-server/jupyter_server/issues/743
             {"ip": ""},
             "http://localhost:8888/?token=<generated>",
             "http://127.0.0.1:8888/?token=<generated>",
             "http://localhost:8888/";,
         ),
+        (
+            # https://github.com/jupyterlab/jupyterlab/issues/15520
+            {"ip": "0.0.0.0"},
+            "http://0.0.0.0:8888/?token=<generated>",
+            "http://127.0.0.1:8888/?token=<generated>",
+            "http://0.0.0.0:8888/";,
+        ),
+        (
+            # https://github.com/jupyterlab/jupyterlab/issues/15520
+            {"ip": "::"},
+            "http://[::]:8888/?token=<generated>",
+            "http://[::1]:8888/?token=<generated>",
+            "http://[::]:8888/";,
+        ),
     ],
 )
 def test_urls(config, public_url, local_url, connection_url):
@@ -343,6 +358,61 @@
     ServerApp.clear_instance()
 
 
[email protected](
+    "config,connect_url",
+    [
+        # Non-wildcard addresses: mirrors display_url.
+        (
+            {"ip": ""},
+            "http://localhost:8888/?token=<generated>\n    
http://127.0.0.1:8888/?token=<generated>",
+        ),
+        (
+            {"ip": "127.0.0.1"},
+            "http://127.0.0.1:8888/?token=<generated>",
+        ),
+        # Sockets: mirrors display_url.
+        (
+            {"sock": "/tmp/jp-test.sock"},
+            "http+unix://%2Ftmp%2Fjp-test.sock/?token=<generated>",
+        ),
+        # Wildcard addresses are not connectable: use the machine's
+        # hostname instead, with a trailing note that any address works.
+        (
+            {"ip": "0.0.0.0"},
+            "http://myhost:8888/?token=<generated>\n"
+            "    http://127.0.0.1:8888/?token=<generated>\n"
+            "The server is listening on all interfaces, "
+            "so any hostname or IP of this machine will work.",
+        ),
+        # Wildcard but ipv6
+        (
+            {"ip": "::"},
+            "http://myhost:8888/?token=<generated>\n"
+            "    http://[::1]:8888/?token=<generated>\n"
+            "The server is listening on all interfaces, "
+            "so any hostname or IP of this machine will work.",
+        ),
+    ],
+)
+def test_connect_url(config, connect_url):
+    # Verify we're working with a clean instance.
+    ServerApp.clear_instance()
+    serverapp = ServerApp.instance(**config)
+    serverapp.init_configurables()
+    token = serverapp.identity_provider.token
+    if serverapp.identity_provider.token_generated:
+        connect_url = connect_url.replace("<generated>", token)
+
+    with patch("socket.gethostname", return_value="myhost"):
+        assert serverapp.connect_url == connect_url
+
+    # The wildcard address is never advertised as connectable.
+    assert "0.0.0.0" not in serverapp.connect_url
+    assert "[::]" not in serverapp.connect_url
+    # Cleanup singleton after test.
+    ServerApp.clear_instance()
+
+
 # Preferred dir tests
 # ----------------------------------------------------------------------------
 @pytest.mark.filterwarnings("ignore::FutureWarning")
@@ -664,6 +734,14 @@
     assert _has_tornado_web_authenticated(method) == expected
 
 
+def test_find_http_port_zero_resolves_real_port(jp_configurable_serverapp):
+    """port=0 should resolve to the real OS-assigned port, not stay 0 
(#1650)."""
+    app = jp_configurable_serverapp()
+    app.port = 0
+    app._find_http_port()
+    assert app.port != 0
+
+
 def test_bind_http_server_tcp_success(jp_configurable_serverapp):
     """Normal case: listen succeeds, returns True."""
     app = jp_configurable_serverapp()

Reply via email to