From efca73b7294a8c966c50073b18651c8c4c44ab45 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 14:13:07 +0100 Subject: [PATCH 01/21] Update node dependencies --- package.json | 2 +- yarn.lock | 839 ++++++++++++++++++++++++++------------------------- 2 files changed, 421 insertions(+), 420 deletions(-) diff --git a/package.json b/package.json index 2872459..908fcd6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@babel/register": "^7.23.7", "@types/node": "^24.3.0", "all-contributors-cli": "^6.26.1", - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "prettier": "3.6.2", "ts-loader": "^9.5.1", "typescript": "^5.4.5", diff --git a/yarn.lock b/yarn.lock index 984a4a1..5963f50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,6 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" @@ -19,39 +11,39 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" - integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== +"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" + integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== "@babel/core@^7.24.5": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.3.tgz#aceddde69c5d1def69b839d09efa3e3ff59c97cb" - integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== dependencies: - "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.3" + "@babel/generator" "^7.28.5" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-module-transforms" "^7.28.3" - "@babel/helpers" "^7.28.3" - "@babel/parser" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" - integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== +"@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== dependencies: - "@babel/parser" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" @@ -74,26 +66,26 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz#3e747434ea007910c320c4d39a6b46f20f371d46" - integrity sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg== +"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3", "@babel/helper-create-class-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" + integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.28.5" "@babel/helper-optimise-call-expression" "^7.27.1" "@babel/helper-replace-supers" "^7.27.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/traverse" "^7.28.5" semver "^6.3.1" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" - integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== dependencies: - "@babel/helper-annotate-as-pure" "^7.27.1" - regexpu-core "^6.2.0" + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" semver "^6.3.1" "@babel/helper-define-polyfill-provider@^0.6.5": @@ -112,13 +104,13 @@ resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-member-expression-to-functions@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" - integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== +"@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== dependencies: - "@babel/traverse" "^7.27.1" - "@babel/types" "^7.27.1" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" "@babel/helper-module-imports@^7.27.1": version "7.27.1" @@ -180,10 +172,10 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" - integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== "@babel/helper-validator-option@^7.27.1": version "7.27.1" @@ -199,28 +191,28 @@ "@babel/traverse" "^7.28.3" "@babel/types" "^7.28.2" -"@babel/helpers@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.3.tgz#b83156c0a2232c133d1b535dd5d3452119c7e441" - integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw== +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== dependencies: "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.4" -"@babel/parser@^7.27.2", "@babel/parser@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" - integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== +"@babel/parser@^7.27.2", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.5" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" - integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" - "@babel/traverse" "^7.27.1" + "@babel/traverse" "^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": version "7.27.1" @@ -326,10 +318,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoping@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz#e7c50cbacc18034f210b93defa89638666099451" - integrity sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q== +"@babel/plugin-transform-block-scoping@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz#e0d3af63bd8c80de2e567e690a54e84d85eb16f6" + integrity sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -349,17 +341,17 @@ "@babel/helper-create-class-features-plugin" "^7.28.3" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-classes@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz#598297260343d0edbd51cb5f5075e07dee91963a" - integrity sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg== +"@babel/plugin-transform-classes@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-globals" "^7.28.0" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-replace-supers" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/traverse" "^7.28.4" "@babel/plugin-transform-computed-properties@^7.27.1": version "7.27.1" @@ -369,13 +361,13 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/template" "^7.27.1" -"@babel/plugin-transform-destructuring@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz#0f156588f69c596089b7d5b06f5af83d9aa7f97a" - integrity sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A== +"@babel/plugin-transform-destructuring@^7.28.0", "@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" - "@babel/traverse" "^7.28.0" + "@babel/traverse" "^7.28.5" "@babel/plugin-transform-dotall-regex@^7.27.1": version "7.27.1" @@ -415,10 +407,10 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-destructuring" "^7.28.0" -"@babel/plugin-transform-exponentiation-operator@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" - integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== +"@babel/plugin-transform-exponentiation-operator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz#7cc90a8170e83532676cfa505278e147056e94fe" + integrity sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -460,10 +452,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-logical-assignment-operators@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" - integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== +"@babel/plugin-transform-logical-assignment-operators@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz#d028fd6db8c081dee4abebc812c2325e24a85b0e" + integrity sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -490,15 +482,15 @@ "@babel/helper-module-transforms" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-systemjs@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" - integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== +"@babel/plugin-transform-modules-systemjs@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz#7439e592a92d7670dfcb95d0cbc04bd3e64801d2" + integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== dependencies: - "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-module-transforms" "^7.28.3" "@babel/helper-plugin-utils" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@babel/traverse" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.5" "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" @@ -537,16 +529,16 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-object-rest-spread@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz#d23021857ffd7cd809f54d624299b8086402ed8d" - integrity sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA== +"@babel/plugin-transform-object-rest-spread@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz#9ee1ceca80b3e6c4bac9247b2149e36958f7f98d" + integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-destructuring" "^7.28.0" "@babel/plugin-transform-parameters" "^7.27.7" - "@babel/traverse" "^7.28.0" + "@babel/traverse" "^7.28.4" "@babel/plugin-transform-object-super@^7.27.1": version "7.27.1" @@ -563,10 +555,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-optional-chaining@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" - integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz#8238c785f9d5c1c515a90bf196efb50d075a4b26" + integrity sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" @@ -602,10 +594,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz#b8eee0f8aed37704bbcc932fd0b1a0a34d0b7344" - integrity sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A== +"@babel/plugin-transform-regenerator@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz#9d3fa3bebb48ddd0091ce5729139cd99c67cea51" + integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -660,13 +652,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typescript@^7.27.1": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz#796cbd249ab56c18168b49e3e1d341b72af04a6b" - integrity sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg== +"@babel/plugin-transform-typescript@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72" + integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.28.5" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-syntax-typescript" "^7.27.1" @@ -703,15 +695,15 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/preset-env@^7.24.5": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.3.tgz#2b18d9aff9e69643789057ae4b942b1654f88187" - integrity sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg== + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.5.tgz#82dd159d1563f219a1ce94324b3071eb89e280b0" + integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg== dependencies: - "@babel/compat-data" "^7.28.0" + "@babel/compat-data" "^7.28.5" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-validator-option" "^7.27.1" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" @@ -724,42 +716,42 @@ "@babel/plugin-transform-async-generator-functions" "^7.28.0" "@babel/plugin-transform-async-to-generator" "^7.27.1" "@babel/plugin-transform-block-scoped-functions" "^7.27.1" - "@babel/plugin-transform-block-scoping" "^7.28.0" + "@babel/plugin-transform-block-scoping" "^7.28.5" "@babel/plugin-transform-class-properties" "^7.27.1" "@babel/plugin-transform-class-static-block" "^7.28.3" - "@babel/plugin-transform-classes" "^7.28.3" + "@babel/plugin-transform-classes" "^7.28.4" "@babel/plugin-transform-computed-properties" "^7.27.1" - "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-destructuring" "^7.28.5" "@babel/plugin-transform-dotall-regex" "^7.27.1" "@babel/plugin-transform-duplicate-keys" "^7.27.1" "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" "@babel/plugin-transform-dynamic-import" "^7.27.1" "@babel/plugin-transform-explicit-resource-management" "^7.28.0" - "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator" "^7.28.5" "@babel/plugin-transform-export-namespace-from" "^7.27.1" "@babel/plugin-transform-for-of" "^7.27.1" "@babel/plugin-transform-function-name" "^7.27.1" "@babel/plugin-transform-json-strings" "^7.27.1" "@babel/plugin-transform-literals" "^7.27.1" - "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.5" "@babel/plugin-transform-member-expression-literals" "^7.27.1" "@babel/plugin-transform-modules-amd" "^7.27.1" "@babel/plugin-transform-modules-commonjs" "^7.27.1" - "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.28.5" "@babel/plugin-transform-modules-umd" "^7.27.1" "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" "@babel/plugin-transform-new-target" "^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" "@babel/plugin-transform-numeric-separator" "^7.27.1" - "@babel/plugin-transform-object-rest-spread" "^7.28.0" + "@babel/plugin-transform-object-rest-spread" "^7.28.4" "@babel/plugin-transform-object-super" "^7.27.1" "@babel/plugin-transform-optional-catch-binding" "^7.27.1" - "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.28.5" "@babel/plugin-transform-parameters" "^7.27.7" "@babel/plugin-transform-private-methods" "^7.27.1" "@babel/plugin-transform-private-property-in-object" "^7.27.1" "@babel/plugin-transform-property-literals" "^7.27.1" - "@babel/plugin-transform-regenerator" "^7.28.3" + "@babel/plugin-transform-regenerator" "^7.28.4" "@babel/plugin-transform-regexp-modifiers" "^7.27.1" "@babel/plugin-transform-reserved-words" "^7.27.1" "@babel/plugin-transform-shorthand-properties" "^7.27.1" @@ -788,15 +780,15 @@ esutils "^2.0.2" "@babel/preset-typescript@^7.24.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" - integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c" + integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-validator-option" "^7.27.1" "@babel/plugin-syntax-jsx" "^7.27.1" "@babel/plugin-transform-modules-commonjs" "^7.27.1" - "@babel/plugin-transform-typescript" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.28.5" "@babel/register@^7.23.7": version "7.28.3" @@ -810,9 +802,9 @@ source-map-support "^0.5.16" "@babel/runtime@^7.18.9", "@babel/runtime@^7.7.6": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.3.tgz#75c5034b55ba868121668be5d5bb31cc64e6e61a" - integrity sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== "@babel/template@^7.27.1", "@babel/template@^7.27.2": version "7.27.2" @@ -823,161 +815,161 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434" - integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ== +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== dependencies: "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.3" + "@babel/generator" "^7.28.5" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.3" + "@babel/parser" "^7.28.5" "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.5" debug "^4.3.1" -"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.4.4": - version "7.28.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" - integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.4.4": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== dependencies: "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@discoveryjs/json-ext@^0.6.1": version "0.6.3" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== -"@esbuild/aix-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" - integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== - -"@esbuild/android-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c" - integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== - -"@esbuild/android-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419" - integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== - -"@esbuild/android-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683" - integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== - -"@esbuild/darwin-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae" - integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== - -"@esbuild/darwin-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be" - integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== - -"@esbuild/freebsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca" - integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== - -"@esbuild/freebsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab" - integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== - -"@esbuild/linux-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b" - integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== - -"@esbuild/linux-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37" - integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== - -"@esbuild/linux-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4" - integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== - -"@esbuild/linux-loong64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0" - integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== - -"@esbuild/linux-mips64el@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5" - integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== - -"@esbuild/linux-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db" - integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== - -"@esbuild/linux-riscv64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547" - integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== - -"@esbuild/linux-s390x@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830" - integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== - -"@esbuild/linux-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f" - integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== - -"@esbuild/netbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548" - integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== - -"@esbuild/netbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52" - integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== - -"@esbuild/openbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935" - integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== - -"@esbuild/openbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf" - integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== - -"@esbuild/openharmony-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314" - integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== - -"@esbuild/sunos-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e" - integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== - -"@esbuild/win32-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b" - integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== - -"@esbuild/win32-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3" - integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== - -"@esbuild/win32-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" - integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== +"@esbuild/aix-ppc64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz#1d8be43489a961615d49e037f1bfa0f52a773737" + integrity sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A== + +"@esbuild/android-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz#bd1763194aad60753fa3338b1ba9bda974b58724" + integrity sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ== + +"@esbuild/android-arm@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.0.tgz#69c7b57f02d3b3618a5ba4f82d127b57665dc397" + integrity sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ== + +"@esbuild/android-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.0.tgz#6ea22b5843acb23243d0126c052d7d3b6a11ca90" + integrity sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q== + +"@esbuild/darwin-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz#5ad7c02bc1b1a937a420f919afe40665ba14ad1e" + integrity sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg== + +"@esbuild/darwin-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz#48470c83c5fd6d1fc7c823c2c603aeee96e101c9" + integrity sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g== + +"@esbuild/freebsd-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz#d5a8effd8b0be7be613cd1009da34d629d4c2457" + integrity sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw== + +"@esbuild/freebsd-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz#9bde638bda31aa244d6d64dbafafb41e6e799bcc" + integrity sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g== + +"@esbuild/linux-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz#96008c3a207d8ca495708db714c475ea5bf7e2af" + integrity sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ== + +"@esbuild/linux-arm@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz#9b47cb0f222e567af316e978c7f35307db97bc0e" + integrity sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ== + +"@esbuild/linux-ia32@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz#d1e1e38d406cbdfb8a49f4eca0c25bbc344e18cc" + integrity sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw== + +"@esbuild/linux-loong64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz#c13bc6a53e3b69b76f248065bebee8415b44dfce" + integrity sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg== + +"@esbuild/linux-mips64el@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz#05f8322eb0a96ce1bfbc59691abe788f71e2d217" + integrity sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg== + +"@esbuild/linux-ppc64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz#6fc5e7af98b4fb0c6a7f0b73ba837ce44dc54980" + integrity sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA== + +"@esbuild/linux-riscv64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz#508afa9f69a3f97368c0bf07dd894a04af39d86e" + integrity sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ== + +"@esbuild/linux-s390x@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz#21fda656110ee242fc64f87a9e0b0276d4e4ec5b" + integrity sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w== + +"@esbuild/linux-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz#1758a85dcc09b387fd57621643e77b25e0ccba59" + integrity sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw== + +"@esbuild/netbsd-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz#a0131159f4db6e490da35cc4bb51ef0d03b7848a" + integrity sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w== + +"@esbuild/netbsd-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz#6f4877d7c2ba425a2b80e4330594e0b43caa2d7d" + integrity sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA== + +"@esbuild/openbsd-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz#cbefbd4c2f375cebeb4f965945be6cf81331bd01" + integrity sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ== + +"@esbuild/openbsd-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz#31fa9e8649fc750d7c2302c8b9d0e1547f57bc84" + integrity sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A== + +"@esbuild/openharmony-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz#03727780f1fdf606e7b56193693a715d9f1ee001" + integrity sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA== + +"@esbuild/sunos-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz#866a35f387234a867ced35af8906dfffb073b9ff" + integrity sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA== + +"@esbuild/win32-arm64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz#53de43a9629b8a34678f28cd56cc104db1b67abb" + integrity sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg== + +"@esbuild/win32-ia32@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz#924d2aed8692fea5d27bfb6500f9b8b9c1a34af4" + integrity sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ== + +"@esbuild/win32-x64@0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz#64995295227e001f2940258617c6674efb3ac48d" + integrity sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg== "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" @@ -987,6 +979,14 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -1006,17 +1006,17 @@ integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.30" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" - integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" "@simplewebauthn/browser@^13.0.0": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-13.1.2.tgz#e904373854616e469c4c1ab9d8c4f704e9ac6db1" - integrity sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw== + version "13.2.2" + resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-13.2.2.tgz#4cde38c4c6969a039c23c2a3d931ecb69f937910" + integrity sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA== "@types/eslint-scope@^3.7.7": version "3.7.7" @@ -1045,11 +1045,11 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@*", "@types/node@^24.3.0": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" - integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== + version "24.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" + integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== dependencies: - undici-types "~7.10.0" + undici-types "~7.16.0" "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" @@ -1202,7 +1202,7 @@ acorn-import-phases@^1.0.3: resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== -acorn@^8.14.0, acorn@^8.15.0: +acorn@^8.15.0: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -1297,6 +1297,11 @@ babel-plugin-polyfill-regenerator@^0.6.5: dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" +baseline-browser-mapping@^2.8.25: + version "2.8.30" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz#5c7420acc2fd20f3db820a40c6521590a671d137" + integrity sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA== + braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -1304,15 +1309,16 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0, browserslist@^4.25.3: - version "4.25.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5" - integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ== +browserslist@^4.24.0, browserslist@^4.26.3, browserslist@^4.28.0: + version "4.28.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" + integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ== dependencies: - caniuse-lite "^1.0.30001735" - electron-to-chromium "^1.5.204" - node-releases "^2.0.19" - update-browserslist-db "^1.1.3" + baseline-browser-mapping "^2.8.25" + caniuse-lite "^1.0.30001754" + electron-to-chromium "^1.5.249" + node-releases "^2.0.27" + update-browserslist-db "^1.1.4" buffer-from@^1.0.0: version "1.1.2" @@ -1324,10 +1330,10 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001735: - version "1.0.30001737" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc" - integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw== +caniuse-lite@^1.0.30001754: + version "1.0.30001756" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz#fe80104631102f88e58cad8aa203a2c3e5ec9ebd" + integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A== chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" @@ -1415,11 +1421,11 @@ convert-source-map@^2.0.0: integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== core-js-compat@^3.43.0: - version "3.45.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.45.1.tgz#424f3f4af30bf676fd1b67a579465104f64e9c7a" - integrity sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA== + version "3.47.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3" + integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ== dependencies: - browserslist "^4.25.3" + browserslist "^4.28.0" cross-spawn@^7.0.3: version "7.0.6" @@ -1431,9 +1437,9 @@ cross-spawn@^7.0.3: which "^2.0.1" debug@^4.1.0, debug@^4.3.1, debug@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" - integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" @@ -1447,10 +1453,10 @@ didyoumean@^1.2.1: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -electron-to-chromium@^1.5.204: - version "1.5.208" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz#609c29502fd7257b4d721e3446f3ae391a0ca1b3" - integrity sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg== +electron-to-chromium@^1.5.249: + version "1.5.259" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz#d4393167ec14c5a046cebaec3ddf3377944ce965" + integrity sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ== emoji-regex@^8.0.0: version "8.0.0" @@ -1466,46 +1472,46 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.3: tapable "^2.2.0" envinfo@^7.14.0: - version "7.14.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" - integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + version "7.20.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.20.0.tgz#3fd9de69fb6af3e777a017dfa033676368d67dd7" + integrity sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg== es-module-lexer@^1.2.1: version "1.7.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== -esbuild@^0.25.0: - version "0.25.9" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976" - integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g== +esbuild@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.0.tgz#db983bed6f76981361c92f50cf6a04c66f7b3e1d" + integrity sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.9" - "@esbuild/android-arm" "0.25.9" - "@esbuild/android-arm64" "0.25.9" - "@esbuild/android-x64" "0.25.9" - "@esbuild/darwin-arm64" "0.25.9" - "@esbuild/darwin-x64" "0.25.9" - "@esbuild/freebsd-arm64" "0.25.9" - "@esbuild/freebsd-x64" "0.25.9" - "@esbuild/linux-arm" "0.25.9" - "@esbuild/linux-arm64" "0.25.9" - "@esbuild/linux-ia32" "0.25.9" - "@esbuild/linux-loong64" "0.25.9" - "@esbuild/linux-mips64el" "0.25.9" - "@esbuild/linux-ppc64" "0.25.9" - "@esbuild/linux-riscv64" "0.25.9" - "@esbuild/linux-s390x" "0.25.9" - "@esbuild/linux-x64" "0.25.9" - "@esbuild/netbsd-arm64" "0.25.9" - "@esbuild/netbsd-x64" "0.25.9" - "@esbuild/openbsd-arm64" "0.25.9" - "@esbuild/openbsd-x64" "0.25.9" - "@esbuild/openharmony-arm64" "0.25.9" - "@esbuild/sunos-x64" "0.25.9" - "@esbuild/win32-arm64" "0.25.9" - "@esbuild/win32-ia32" "0.25.9" - "@esbuild/win32-x64" "0.25.9" + "@esbuild/aix-ppc64" "0.27.0" + "@esbuild/android-arm" "0.27.0" + "@esbuild/android-arm64" "0.27.0" + "@esbuild/android-x64" "0.27.0" + "@esbuild/darwin-arm64" "0.27.0" + "@esbuild/darwin-x64" "0.27.0" + "@esbuild/freebsd-arm64" "0.27.0" + "@esbuild/freebsd-x64" "0.27.0" + "@esbuild/linux-arm" "0.27.0" + "@esbuild/linux-arm64" "0.27.0" + "@esbuild/linux-ia32" "0.27.0" + "@esbuild/linux-loong64" "0.27.0" + "@esbuild/linux-mips64el" "0.27.0" + "@esbuild/linux-ppc64" "0.27.0" + "@esbuild/linux-riscv64" "0.27.0" + "@esbuild/linux-s390x" "0.27.0" + "@esbuild/linux-x64" "0.27.0" + "@esbuild/netbsd-arm64" "0.27.0" + "@esbuild/netbsd-x64" "0.27.0" + "@esbuild/openbsd-arm64" "0.27.0" + "@esbuild/openbsd-x64" "0.27.0" + "@esbuild/openharmony-arm64" "0.27.0" + "@esbuild/sunos-x64" "0.27.0" + "@esbuild/win32-arm64" "0.27.0" + "@esbuild/win32-ia32" "0.27.0" + "@esbuild/win32-x64" "0.27.0" escalade@^3.2.0: version "3.2.0" @@ -1567,9 +1573,9 @@ fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastest-levenshtein@^1.0.12: version "1.0.16" @@ -1695,7 +1701,7 @@ interpret@^3.1.1: resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== -is-core-module@^2.16.0: +is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -1743,16 +1749,11 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -jsesc@^3.0.2: +jsesc@^3.0.2, jsesc@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -jsesc@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - json-fixer@^1.6.8: version "1.6.15" resolved "https://registry.yarnpkg.com/json-fixer/-/json-fixer-1.6.15.tgz#f1f03b6771fcb383695d458c53e50b10999fba7f" @@ -1782,10 +1783,10 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== +loader-runner@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" + integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== locate-path@^3.0.0: version "3.0.0" @@ -1879,10 +1880,10 @@ node-fetch@^2.6.0: dependencies: whatwg-url "^5.0.0" -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== onetime@^5.1.0: version "5.1.2" @@ -2010,10 +2011,10 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== dependencies: regenerate "^1.4.2" @@ -2022,29 +2023,29 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regexpu-core@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" - integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" + regenerate-unicode-properties "^10.2.2" regjsgen "^0.8.0" - regjsparser "^0.12.0" + regjsparser "^0.13.0" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" + unicode-match-property-value-ecmascript "^2.2.1" regjsgen@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== -regjsparser@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" - integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== +regjsparser@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.0.tgz#01f8351335cf7898d43686bc74d2dd71c847ecc0" + integrity sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q== dependencies: - jsesc "~3.0.2" + jsesc "~3.1.0" require-directory@^2.1.1: version "2.1.1" @@ -2074,11 +2075,11 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve@^1.20.0, resolve@^1.22.10: - version "1.22.10" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" - integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== dependencies: - is-core-module "^2.16.0" + is-core-module "^2.16.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -2112,10 +2113,10 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^4.3.0, schema-utils@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" - integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== +schema-utils@^4.3.0, schema-utils@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -2133,9 +2134,9 @@ semver@^6.3.1: integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.4: - version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== serialize-javascript@^6.0.2: version "6.0.2" @@ -2226,10 +2227,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tapable@^2.1.1, tapable@^2.2.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.3.tgz#4b67b635b2d97578a06a2713d2f04800c237e99b" - integrity sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg== +tapable@^2.2.0, tapable@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" + integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== terser-webpack-plugin@^5.3.11: version "5.3.14" @@ -2243,12 +2244,12 @@ terser-webpack-plugin@^5.3.11: terser "^5.31.1" terser@^5.31.1: - version "5.43.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.43.1.tgz#88387f4f9794ff1a29e7ad61fb2932e25b4fdb6d" - integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg== + version "5.44.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c" + integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw== dependencies: "@jridgewell/source-map" "^0.3.3" - acorn "^8.14.0" + acorn "^8.15.0" commander "^2.20.0" source-map-support "~0.5.20" @@ -2298,14 +2299,14 @@ type-fest@^0.21.3: integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== typescript@^5.4.5: - version "5.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" - integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== -undici-types@~7.10.0: - version "7.10.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" - integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" @@ -2320,25 +2321,25 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" - integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== -update-browserslist-db@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" - integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== +update-browserslist-db@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a" + integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A== dependencies: escalade "^3.2.0" picocolors "^1.1.1" -watchpack@^2.4.1: +watchpack@^2.4.4: version "2.4.4" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== @@ -2385,9 +2386,9 @@ webpack-sources@^3.3.3: integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== webpack@^5.94.0: - version "5.101.3" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346" - integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A== + version "5.103.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.103.0.tgz#17a7c5a5020d5a3a37c118d002eade5ee2c6f3da" + integrity sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.8" @@ -2397,7 +2398,7 @@ webpack@^5.94.0: "@webassemblyjs/wasm-parser" "^1.14.1" acorn "^8.15.0" acorn-import-phases "^1.0.3" - browserslist "^4.24.0" + browserslist "^4.26.3" chrome-trace-event "^1.0.2" enhanced-resolve "^5.17.3" es-module-lexer "^1.2.1" @@ -2406,13 +2407,13 @@ webpack@^5.94.0: glob-to-regexp "^0.4.1" graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" + loader-runner "^4.3.1" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^4.3.2" - tapable "^2.1.1" + schema-utils "^4.3.3" + tapable "^2.3.0" terser-webpack-plugin "^5.3.11" - watchpack "^2.4.1" + watchpack "^2.4.4" webpack-sources "^3.3.3" whatwg-url@^5.0.0: From db3b8f0bc89767e2016b44e730b1677eef71b136 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 14:16:49 +0100 Subject: [PATCH 02/21] Remove deprecated pip flag --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8966080..da70e05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,6 @@ env: FORCE_COLOR: "1" # Make tools pretty. TOX_TESTENV_PASSENV: FORCE_COLOR PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_PYTHON_VERSION_WARNING: "1" jobs: qa_javascript: From 362c8f0ebebc9cc37fdf0e88cc08a497c629da8e Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 18 Oct 2025 16:26:24 +0200 Subject: [PATCH 03/21] Add ability to signal unknown credentials to the browser Refs: #38 --- CHANGELOG.md | 8 +- client/src/auth.ts | 55 +++++- client/src/types.ts | 1 + src/django_otp_webauthn/settings.py | 8 + .../templatetags/otp_webauthn.py | 1 + tests/e2e/conftest.py | 22 +++ tests/e2e/test_credential_authentication.py | 180 ++++++++++++++++++ tests/unit/test_templatetags.py | 10 + 8 files changed, 282 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65bb8ca..9890718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Nothing yet +- **New feature (experimental):** the browser will now be signaled to remove an unknown credential after a failed authentication attempt. This is controlled by the new `OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL` setting, which defaults to `True`. If set to `False`, the browser will not be signaled. + - The purpose of this is to improve user experience by removing credentials that are no longer valid from the users' device, stopping the user from being prompted to use this credential in the future. + - The exact response of browsers to the signal varies, most browsers tested appear to ignore this signal and thus this feature has no effect. + - This uses a draft feature defined the WebAuthn L3 specification: https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#sctn-signal-methods. + - It works on recent versions of Chrome, Edge and Safari but not Firefox (as of October 2025). + - Read more about the browser API used: [`PublicKeyCredential.signalUnknownCredential` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalUnknownCredential_static). + - This feature is experimental because not all browsers support it properly yet. The specification is also still in draft status and may change in the future. ### Changed diff --git a/client/src/auth.ts b/client/src/auth.ts index fa9065b..6b51ebf 100644 --- a/client/src/auth.ts +++ b/client/src/auth.ts @@ -85,10 +85,11 @@ import { return; } + const responseJson = await response.json(); let attResp; try { attResp = await startAuthentication({ - optionsJSON: await response.json(), + optionsJSON: responseJson, useBrowserAutofill: true, }); } catch (error: unknown) { @@ -149,6 +150,9 @@ import { // Handle failed verification if (!verificationResp.ok && "detail" in verificationJSON) { + if (verificationResp.status === 404) { + signalPasskeyMissing(attResp.rawId, responseJson.rpId); + } loginField.dispatchEvent( new CustomEvent(EVENT_VERIFICATION_FAILED, { detail: { @@ -232,6 +236,8 @@ import { }, }); + const responseJson = await response.json(); + if (!response.ok) { await setPasskeyVerifyState({ buttonDisabled: false, @@ -258,7 +264,7 @@ import { try { attResp = await startAuthentication({ - optionsJSON: await response.json(), + optionsJSON: responseJson, useBrowserAutofill: false, }); } catch (error: unknown) { @@ -363,6 +369,9 @@ import { const verificationJSON = await verificationResp.json(); if (!verificationResp.ok) { + if (verificationResp.status === 404 && attResp) { + signalPasskeyMissing(attResp.rawId, responseJson.rpId); + } const msg = verificationJSON.detail || gettext("Verification failed. An unknown error occurred."); @@ -471,6 +480,48 @@ import { } } + /* Uses the `PublicKeyCredential.signalUnknownCredential` API to inform the browser + * that the Passkey that was used is not recognized by the server, prompting the browser to delete it from its stored credentials. + * This function is a no-op if the API is not supported by the browser. + */ + async function signalPasskeyMissing( + credentialId: string, + rpId: string, + ): Promise { + if (!config.removeUnknownCredential) { + console.trace( + "Not signaling unknown credential to the browser as per configuration.", + ); + return; + } + if (!("signalUnknownCredential" in PublicKeyCredential)) { + console.trace( + "PublicKeyCredential.signalUnknownCredential is not supported by this browser. Won't signal.", + ); + return; + } + + try { + await (PublicKeyCredential as any).signalUnknownCredential({ + rpId, + credentialId, + }); + console.trace( + // Important: 'Credential not found' is used as a needle for automated tests that check that the signaling happened. + "Credential not found. Requested browser remove credential.", + { + rpId, + credentialId, + }, + ); + } catch (error) { + console.error( + "Error while signaling unknown credential to the browser", + error, + ); + } + } + async function setPasskeyVerificationVisible( visible: boolean, ): Promise { diff --git a/client/src/types.ts b/client/src/types.ts index 9e59853..8edd6e4 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -32,4 +32,5 @@ export type Config = { nextFieldSelector: string; csrfToken: string; + removeUnknownCredential: boolean; }; diff --git a/src/django_otp_webauthn/settings.py b/src/django_otp_webauthn/settings.py index 5cb29e4..9afe2c6 100644 --- a/src/django_otp_webauthn/settings.py +++ b/src/django_otp_webauthn/settings.py @@ -106,6 +106,14 @@ class AppSettings: accessibility guidelines regarding timeouts. See https://www.w3.org/TR/WCAG22/#enough-time. """ + OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL = True + """If ``True``, when the user tries to + authenticate using a credential that does not exist on the + server, the client-side script will signal the browser the Passkey does not + exist anymore. It is up to browsers to implement this signal, but the + intention is for them to (automatically) remove the Passkey from the user's + device and not have show it as an option to the user again.""" + def __getattribute__(self, __name: str): # Check if a Django project settings should override the app default. # In order to avoid returning any random properties of the django settings, we inspect the prefix firstly. diff --git a/src/django_otp_webauthn/templatetags/otp_webauthn.py b/src/django_otp_webauthn/templatetags/otp_webauthn.py index 89dd762..5175dcc 100644 --- a/src/django_otp_webauthn/templatetags/otp_webauthn.py +++ b/src/django_otp_webauthn/templatetags/otp_webauthn.py @@ -15,6 +15,7 @@ def get_configuration(request: HttpRequest, extra_options: dict = None) -> dict: "autocompleteLoginFieldSelector": None, "nextFieldSelector": "input[name='next']", "csrfToken": csrf.get_token(request), + "removeUnknownCredential": app_settings.OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL, "beginAuthenticationUrl": reverse( "otp_webauthn:credential-authentication-begin" ), diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 286edce..c43eeee 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -1,4 +1,5 @@ import random +import re from concurrent.futures import Future import pytest @@ -78,6 +79,27 @@ def _return(): return _wait_for_javascript_event +@pytest.fixture +def wait_for_console_message(page): + """Returns a function that blocks until a certain console message has been posted.""" + + def _wait_for_console_message(message_text_regex: str, *, level="log"): + future = FutureWrapper() + + def _handle_console_message(msg): + if msg.type == level and re.match(message_text_regex, msg.text): + future.set_result(msg) + + page.on("console", _handle_console_message) + + def _return(): + return future.get_result() + + return _return + + return _wait_for_console_message + + @pytest.fixture def virtual_authenticator(cdpsession): def _get_authenticator(authenticator: VirtualAuthenticator): diff --git a/tests/e2e/test_credential_authentication.py b/tests/e2e/test_credential_authentication.py index 7652023..df55b4d 100644 --- a/tests/e2e/test_credential_authentication.py +++ b/tests/e2e/test_credential_authentication.py @@ -1,5 +1,6 @@ from django.urls import reverse from playwright.sync_api import expect +from webauthn import base64url_to_bytes from tests.e2e.fixtures import StatusEnum, VirtualAuthenticator, VirtualCredential from tests.factories import WebAuthnCredentialFactory @@ -40,6 +41,129 @@ def test_authenticate_credential__internal_passwordless_manual( assert user.username in page.content() +def test_authenticate_credential__manual_signal_no_credential( + live_server, + django_db_serialized_rollback, + page, + user, + virtual_authenticator, + virtual_credential, + wait_for_javascript_event, + wait_for_console_message, +): + """Verify that manually using the 'Authenticate with Passkey' button with a credential + that does not exist signals an unknown credential to the browser.""" + + credential = WebAuthnCredentialFactory(user=user, discoverable=True) + authenticator = virtual_authenticator(VirtualAuthenticator.internal()) + authenticator_id = authenticator["authenticatorId"] + + # Create a virtual credential from our database model + virtual_credential(authenticator_id, VirtualCredential.from_model(credential)) + + # Remove the credential to simulate it was not found / unknown + credential.delete() + + # Go to the login with passkey page + page.goto(live_server.url + reverse("login-passkey")) + await_failure_event = wait_for_javascript_event(JS_EVENT_VERIFICATION_FAILED) + + login_button = page.locator("button#passkey-verification-button") + expect(login_button).to_be_visible() + + login_button.click() + expected_url = live_server.url + reverse( + "otp_webauthn:credential-authentication-complete" + ) + + # Looks for this message in the console log to confirm the signaling happened + await_credential_not_found_message = wait_for_console_message( + r"Credential not found", level="trace" + ) + + with page.expect_response(expected_url, timeout=5000) as response_info: + assert response_info.value.status == 404 # Credential was not found + + # We must wait for the failure event. The signaling happens before the + # failure event, so we know for sure is must have happened. If we don't wait + # in this indirect way, we will block forever waiting for the console + # message and timeout. + await_failure_event() + + # If this does not return, most likely `auth.ts/signalPasskeyMissing()` + # returned early and did not post a console message confirming the + # signaling happened. We check for console messages because there is + # no other way to confirm this using Chrome DevTools Protocol. + message = await_credential_not_found_message() + + # Check the message contains the right arguments - the assumption is that + # if the arguments are correct, the signaling was called correctly. + message_values = message.args[1].json_value() + assert message_values["rpId"] == "localhost" + assert credential.credential_id == base64url_to_bytes( + message_values["credentialId"] + ) + + +def test_authenticate_credential__manual_disable_signal_no_credential( + live_server, + django_db_serialized_rollback, + page, + user, + virtual_authenticator, + virtual_credential, + wait_for_javascript_event, + wait_for_console_message, + settings, +): + """Verify that manually using the 'Authenticate with Passkey' button with a credential with + a credential that does not exist does NOT signal an unknown credential to the browser + when ``OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL = False``.""" + settings.OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL = False + credential = WebAuthnCredentialFactory(user=user, discoverable=True) + authenticator = virtual_authenticator(VirtualAuthenticator.internal()) + authenticator_id = authenticator["authenticatorId"] + + # Create a virtual credential from our database model + virtual_credential(authenticator_id, VirtualCredential.from_model(credential)) + + # Remove the credential to simulate it was not found / unknown + credential.delete() + + # Go to the login with passkey page + page.goto(live_server.url + reverse("login-passkey")) + await_failure_event = wait_for_javascript_event(JS_EVENT_VERIFICATION_FAILED) + + login_button = page.locator("button#passkey-verification-button") + expect(login_button).to_be_visible() + + login_button.click() + expected_url = live_server.url + reverse( + "otp_webauthn:credential-authentication-complete" + ) + + # Looks for this message in the console log to confirm the signaling happened + await_credential_not_found_message = wait_for_console_message( + r"Not signaling unknown credential to the browser as per configuration", + level="trace", + ) + + with page.expect_response(expected_url, timeout=5000) as response_info: + assert response_info.value.status == 404 # Credential was not found + + # We must wait for the failure event. The signaling happens before the + # failure event, so we know for sure is must have happened. If we don't wait + # in this indirect way, we will block forever waiting for the console + # message and timeout. + await_failure_event() + + # If this does not return, most likely `auth.ts/signalPasskeyMissing()` + # returned early and did not post a console message confirming the + # signaling happened. We check for console messages because there is + # no other way to confirm this using Chrome DevTools Protocol. + await_credential_not_found_message() + + def test_authenticate_credential__internal_passwordless_using_autofill( live_server, django_db_serialized_rollback, @@ -68,6 +192,62 @@ def test_authenticate_credential__internal_passwordless_using_autofill( assert user.username in page.content() +def test_authenticate_credential__passwordless_signal_no_credential( + live_server, + django_db_serialized_rollback, + page, + user, + virtual_authenticator, + virtual_credential, + wait_for_javascript_event, + wait_for_console_message, +): + """Verify that using autofill with a credential that does not exist + signals an unknown credential to the browser.""" + credential = WebAuthnCredentialFactory(user=user, discoverable=True) + authenticator = virtual_authenticator(VirtualAuthenticator.internal()) + authenticator_id = authenticator["authenticatorId"] + + # Create a virtual credential from our database model + virtual_credential(authenticator_id, VirtualCredential.from_model(credential)) + + # Remove the credential to simulate it was not found / unknown + credential.delete() + + # Looks for this message in the console log to confirm the signaling happened + await_credential_not_found_message = wait_for_console_message( + r"Credential not found", level="trace" + ) + # Visit the login page with the autofill form + expected_url = live_server.url + reverse( + "otp_webauthn:credential-authentication-complete" + ) + with page.expect_response(expected_url, timeout=5000) as response_info: + page.goto(live_server.url + reverse("auth:login")) + await_failure_event = wait_for_javascript_event(JS_EVENT_VERIFICATION_FAILED) + assert response_info.value.status == 404 # Credential was not found + + # We must wait for the failure event. The signaling happens before the + # failure event, so we know for sure is must have happened. If we don't wait + # in this indirect way, we will block forever waiting for the console + # message and timeout. + await_failure_event() + + # If this does not return, most likely `auth.ts/signalPasskeyMissing()` + # returned early and did not post a console message confirming the + # signaling happened. We check for console messages because there is + # no other way to confirm this using Chrome DevTools Protocol. + message = await_credential_not_found_message() + + # Check the message contains the right arguments - the assumption is that + # if the arguments are correct, the signaling was called correctly. + message_values = message.args[1].json_value() + assert message_values["rpId"] == "localhost" + assert credential.credential_id == base64url_to_bytes( + message_values["credentialId"] + ) + + def test_authenticate_credential__internal_second_factor_fails_when_credential_is_disabled( live_server, django_db_serialized_rollback, diff --git a/tests/unit/test_templatetags.py b/tests/unit/test_templatetags.py index 76ca198..432e999 100644 --- a/tests/unit/test_templatetags.py +++ b/tests/unit/test_templatetags.py @@ -19,6 +19,7 @@ def test_get_configuration__defaults(rf): assert "csrfToken" in configuration assert configuration["nextFieldSelector"] == "input[name='next']" + assert configuration["removeUnknownCredential"] is True # Assert that the URLs are actually resolved assert resolve_url(configuration["beginAuthenticationUrl"]) assert resolve_url(configuration["completeAuthenticationUrl"]) @@ -26,6 +27,15 @@ def test_get_configuration__defaults(rf): assert resolve_url(configuration["completeRegistrationUrl"]) +def test_get_configuration__disable_signal_unknown_credential(rf, settings): + """Test that the configuration reflects the setting to disable signaling unknown credentials.""" + settings.OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL = False + request = rf.get("/") + configuration = get_configuration(request) + + assert configuration["removeUnknownCredential"] is False + + def test_get_configuration__extra_options(rf): """Test that extra options are added to the configuration.""" request = rf.get("/") From 0db000cb1aef6bfabf6234e3d053d0873b8cc95d Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 14:30:02 +0200 Subject: [PATCH 04/21] Show response data in case of assertion failure --- .../integration/test_views_authentication.py | 24 +++++++-------- tests/integration/test_views_registration.py | 30 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/integration/test_views_authentication.py b/tests/integration/test_views_authentication.py index 13e4014..78d9e9b 100644 --- a/tests/integration/test_views_authentication.py +++ b/tests/integration/test_views_authentication.py @@ -19,7 +19,7 @@ def test_authentication__anonymous_user_passwordless_login_disallowed( """Test that an anonymous user is not allowed to begin authentication if passwordless login is disabled.""" settings.OTP_WEBAUTHN_ALLOW_PASSWORDLESS_LOGIN = False response = api_client.post(url) - assert response.status_code == 403 + assert response.status_code == 403, response.data assert response.data["detail"].code == "passwordless_login_disabled" @@ -40,12 +40,12 @@ def test_authentication__http_verbs(api_client, user, url): # OPTIONS should be allowed response = api_client.options(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data # POST should be allowed response = api_client.post(url) # We expect either a 200 or a 400 response (because we are not passing any data) - assert response.status_code == 200 or response.status_code == 400 + assert response.status_code == 200 or response.status_code == 400, response.data # BEGIN AUTHENTICATION VIEW @@ -63,7 +63,7 @@ def test_authentication_begin__anonymous_user_passwordless_login_allowed( ] } response = api_client.post(reverse("otp_webauthn:credential-authentication-begin")) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() session = api_client.session @@ -94,7 +94,7 @@ def test_authentication_begin__logged_in_user( WebAuthnCredentialFactory(user=user, credential_id=credential_id) response = api_client.post(reverse("otp_webauthn:credential-authentication-begin")) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() session = api_client.session @@ -121,7 +121,7 @@ def test_authentication_complete__no_state(api_client, user): url = reverse("otp_webauthn:credential-authentication-complete") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 400 + assert response.status_code == 400, response.data assert response.data["detail"].code == "invalid_state" @@ -131,7 +131,7 @@ def test_authentication_complete__no_reusing_state(api_client, user): api_client.force_login(user) api_client.session["otp_webauthn_authentication_state"] = {"challenge": "challenge"} response = api_client.post(url) - assert response.status_code == 400 + assert response.status_code == 400, response.data # The state should be removed from the session - there is no reusing it assert not api_client.session.get("otp_webauthn_authentication_state") @@ -202,7 +202,7 @@ def test_authentication_complete__anonymous_user_passwordless_login_allowed( data=payload, format="json", ) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() assert data["id"] == credential.pk session = api_client.session @@ -229,7 +229,7 @@ def test_authentication_complete__verify_existing_user(api_client, settings, use data=payload, format="json", ) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() assert data["id"] == credential.pk session = api_client.session @@ -253,7 +253,7 @@ def test_authentication_complete_device_usable__unconfirmed(api_client, user): data=payload, format="json", ) - assert response.status_code == 403 + assert response.status_code == 403, response.data assert response.data["detail"].code == "credential_disabled" session = api_client.session assert "otp_webauthn_authentication_state" not in session @@ -280,7 +280,7 @@ def test_authentication_complete_device_usable__user_disabled( data=payload, format="json", ) - assert response.status_code == 403 + assert response.status_code == 403, response.data assert response.data["detail"].code == "user_disabled" session = api_client.session assert "otp_webauthn_authentication_state" not in session @@ -300,6 +300,6 @@ def test_authentication_complete_get_success_url__understands_next_url_parameter data=payload, format="json", ) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() assert data["redirect_url"] == "/admin" diff --git a/tests/integration/test_views_registration.py b/tests/integration/test_views_registration.py index ec86f4e..8ab2029 100644 --- a/tests/integration/test_views_registration.py +++ b/tests/integration/test_views_registration.py @@ -18,18 +18,18 @@ def test_registration_begin__http_verbs(api_client, user): # OPTIONS should be allowed response = api_client.options(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data # POST should be allowed response = api_client.post(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data @pytest.mark.django_db def test_registration_begin__user_not_authenticated(api_client): url = reverse("otp_webauthn:credential-registration-begin") response = api_client.post(url) - assert response.status_code == 403 + assert response.status_code == 403, response.data @pytest.mark.django_db @@ -42,7 +42,7 @@ def test_registration_begin__no_existing_credentials( url = reverse("otp_webauthn:credential-registration-begin") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() # Response should conform to the schema we established @@ -74,7 +74,7 @@ def test_registration_begin__has_existing_credentials( url = reverse("otp_webauthn:credential-registration-begin") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() # Response should conform to the schema we established @@ -97,7 +97,7 @@ def test_registration_begin__passwordless_login_enabled( url = reverse("otp_webauthn:credential-registration-begin") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() # Response should conform to the schema we established @@ -117,7 +117,7 @@ def test_registration_begin__passwordless_login_disabled( url = reverse("otp_webauthn:credential-registration-begin") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() # Response should conform to the schema we established @@ -134,7 +134,7 @@ def test_registration_begin__keeps_state( url = reverse("otp_webauthn:credential-registration-begin") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data data = response.json() # Response should conform to the schema we established @@ -159,18 +159,18 @@ def test_registration_complete__http_verbs(api_client, user): # OPTIONS should be allowed response = api_client.options(url) - assert response.status_code == 200 + assert response.status_code == 200, response.data # POST should be allowed, though this response will be a 400 because we're not sending the right data response = api_client.post(url) - assert response.status_code == 400 + assert response.status_code == 400, response.data @pytest.mark.django_db def test_registration_complete__user_not_authenticated(api_client): url = reverse("otp_webauthn:credential-registration-complete") response = api_client.post(url) - assert response.status_code == 403 + assert response.status_code == 403, response.data @pytest.mark.django_db @@ -178,7 +178,7 @@ def test_registration_complete__no_state(api_client, user): url = reverse("otp_webauthn:credential-registration-complete") api_client.force_login(user) response = api_client.post(url) - assert response.status_code == 400 + assert response.status_code == 400, response.data assert response.data["detail"].code == "invalid_state" assert api_client.session.get("otp_device_id") is None @@ -189,7 +189,7 @@ def test_registration_complete__no_reusing_state(api_client, user): api_client.force_login(user) api_client.session["otp_webauthn_register_state"] = {"challenge": "challenge"} response = api_client.post(url) - assert response.status_code == 400 + assert response.status_code == 400, response.data # The state should be removed from the session - there is no reusing it assert not api_client.session.get("otp_webauthn_register_state") @@ -230,7 +230,7 @@ def test_registration_complete__valid_response_but_already_verified( "authenticatorAttachment": "platform", } response = api_client.post(url, data=payload, format="json") - assert response.status_code == 200 + assert response.status_code == 200, response.data cred = credential_model.objects.last() assert cred.pk == response.data["id"] @@ -270,7 +270,7 @@ def test_registration_complete__valid_response(api_client, user, credential_mode "authenticatorAttachment": "platform", } response = api_client.post(url, data=payload, format="json") - assert response.status_code == 200 + assert response.status_code == 200, response.data cred = credential_model.objects.first() assert cred.pk == response.data["id"] From c40570faff465b224888400e85d5c743bef9fe7c Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:43:04 +0200 Subject: [PATCH 05/21] Fix coverage run documentation --- docs/contributing/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index 3a4ff23..63bfca4 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -123,7 +123,7 @@ For test coverage, use these commands: .. code-block:: console - coverage run manage.py test + coverage run -m pytest coverage report Generate a visual HTML report in the htmlcov directory with the following command: From 5c57f1def19ebd263cb7a13bbf05f472884d0f51 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:45:21 +0200 Subject: [PATCH 06/21] Add support for signaling current user details and credentials Refs: #38 This uses the `PublicKeyCredential.signalCurrentUserDetails` and `PublicKeyCredential.signalAllAcceptedCredentials` browser apis to 'sync' user details like name and currently registered credentials. This is useful to stop the browser from suggesting a credential was removed and to ensure saved information like a username remains up-to-date. --- client/src/sync_signals.ts | 93 +++++++++++++++++++ client/src/types.ts | 8 ++ client/webpack.base.config.ts | 1 + sandbox/templates/admin/base.html | 7 ++ sandbox/templates/base.html | 3 +- .../sync_signals_scripts.html | 6 ++ .../templatetags/otp_webauthn.py | 54 +++++++++++ src/django_otp_webauthn/utils.py | 14 +++ tests/unit/test_templatetags.py | 75 +++++++++++++++ tests/unit/test_utils.py | 13 +++ 10 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 client/src/sync_signals.ts create mode 100644 sandbox/templates/admin/base.html create mode 100644 src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html diff --git a/client/src/sync_signals.ts b/client/src/sync_signals.ts new file mode 100644 index 0000000..7af38f0 --- /dev/null +++ b/client/src/sync_signals.ts @@ -0,0 +1,93 @@ +import { SyncSignalConfig } from "./types"; + +// Extend the PublicKeyCredentialConstructor interface to include the sync signal methods. +// These don't exist currently in TypeScript's lib.dom.d.ts, so we declare them here. +// At some point this can likely be removed. +interface PublicKeyCredentialConstructor { + signalCurrentUserDetails?(details: { + rpId?: string; + userId?: string; + name?: string; + displayName?: string; + }): Promise; + signalAllAcceptedCredentials(options: { + rpId: string; + userId: string; + allAcceptedCredentialIds: string[]; + }): Promise; +} + +// augment the global constructor type +declare var PublicKeyCredential: PublicKeyCredentialConstructor; + +/** + * Client-side sync signals for WebAuthn credentials. + * + * Reads JSON configuration from a script[id="webauthn-sync-signals-config"] and + * calls the PublicKeyCredential.signalCurrentUserDetails and + * PublicKeyCredential.signalAllAcceptedCredentials browser APIs to update user details + * and to hide removed credentials, so they won't be shown in future authentication prompts. + */ +(() => async () => { + const configScript = document.getElementById( + "otp_webauthn_sync_signals_config", + ); + if (!configScript) { + return; + } + + const config = JSON.parse( + configScript.textContent || "{}", + ) as SyncSignalConfig; + if (!config) { + return; + } + // Remove the config script tag from the DOM now that we've read it + // don't make it available to any other scripts for security/privacy reasons + configScript.remove(); + + // Signal current user details + if ( + typeof PublicKeyCredential === "undefined" || + typeof PublicKeyCredential.signalCurrentUserDetails !== "function" + ) { + console.warn( + "PublicKeyCredential.signalCurrentUserDetails is not supported by this browser.", + ); + return; + } else { + const payload = { + rpId: config.rpId, + userId: config.userId, + name: config.name, + displayName: config.displayName, + }; + await PublicKeyCredential.signalCurrentUserDetails(payload); + console.log( + "[WebAuthn] Signaled current user details to the browser.", + payload, + ); + } + + // Signal all accepted credentials + if ( + typeof PublicKeyCredential === "undefined" || + typeof PublicKeyCredential.signalAllAcceptedCredentials !== "function" + ) { + console.warn( + "PublicKeyCredential.signalAllAcceptedCredentials is not supported by this browser.", + ); + return; + } else { + const payload = { + rpId: config.rpId, + userId: config.userId, + allAcceptedCredentialIds: config.credentialIds, + }; + await PublicKeyCredential.signalAllAcceptedCredentials(payload); + console.log( + "[WebAuthn] Signaled accepted credentials to the browser.", + payload, + ); + } +})()(); diff --git a/client/src/types.ts b/client/src/types.ts index 8edd6e4..48dd39e 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -34,3 +34,11 @@ export type Config = { csrfToken: string; removeUnknownCredential: boolean; }; + +export type SyncSignalConfig = { + rpId: string; + userId: string; + name: string; + displayName: string; + credentialIds: string[]; +}; diff --git a/client/webpack.base.config.ts b/client/webpack.base.config.ts index fabd6ad..40f6c59 100644 --- a/client/webpack.base.config.ts +++ b/client/webpack.base.config.ts @@ -6,6 +6,7 @@ const config: Configuration = { entry: { auth: "./client/src/auth.ts", register: "./client/src/register.ts", + sync_signals: "./client/src/sync_signals.ts", }, module: { rules: [ diff --git a/sandbox/templates/admin/base.html b/sandbox/templates/admin/base.html new file mode 100644 index 0000000..cb29904 --- /dev/null +++ b/sandbox/templates/admin/base.html @@ -0,0 +1,7 @@ +{% extends "admin/base.html" %} +{% load otp_webauthn %} + +{% block extrabody %} + {{ block.super }} + {% render_otp_webauthn_sync_signals_scripts %} +{% endblock extrabody %} diff --git a/sandbox/templates/base.html b/sandbox/templates/base.html index 4e29a2a..0a6fd53 100644 --- a/sandbox/templates/base.html +++ b/sandbox/templates/base.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static otp_webauthn %} @@ -10,5 +10,6 @@ {% block content %}{% endblock content %} + {% render_otp_webauthn_sync_signals_scripts %} diff --git a/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html b/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html new file mode 100644 index 0000000..de3490a --- /dev/null +++ b/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html @@ -0,0 +1,6 @@ +{% load static %}{% spaceless %} + {% if configuration %} + {{ configuration|json_script:"otp_webauthn_sync_signals_config"}} + + {% endif %} +{% endspaceless %} diff --git a/src/django_otp_webauthn/templatetags/otp_webauthn.py b/src/django_otp_webauthn/templatetags/otp_webauthn.py index 5175dcc..3c307fd 100644 --- a/src/django_otp_webauthn/templatetags/otp_webauthn.py +++ b/src/django_otp_webauthn/templatetags/otp_webauthn.py @@ -2,9 +2,13 @@ from django.http import HttpRequest from django.middleware import csrf from django.urls import reverse +from webauthn.helpers import bytes_to_base64url +from django_otp_webauthn.helpers import WebAuthnHelper from django_otp_webauthn.settings import app_settings +from django_otp_webauthn.utils import get_credential_model +WebAuthnCredential = get_credential_model() register = template.Library() @@ -56,3 +60,53 @@ def render_otp_webauthn_register_scripts(context): request = context["request"] context["configuration"] = get_configuration(request) return context + + +@register.inclusion_tag( + "django_otp_webauthn/sync_signals_scripts.html", takes_context=True +) +def render_otp_webauthn_sync_signals_scripts(context): + """Renders a script that calls the + ``PublicKeyCredential.signalCurrentUserDetails`` and + ``PublicKeyCredential.signalAllAcceptedCredentials`` browser apis to update user details + and to hide removed credentials, so they won't be shown in future authentication prompts. + + These scripts are only rendered if the user is authenticated and if a sync is needed. + + A sync can be requested by calling the ``django_otp_webauthn.utils.set_webauthn_sync_signal`` utility function. + """ + request = context["request"] + + # Bail out if the user is not authenticated + if not request.user.is_authenticated: + return {} + + # Bail out if no sync is needed + if "otp_webauthn_sync_needed" not in request.session: + return {} + + # Consume the sync needed flag + request.session.pop("otp_webauthn_sync_needed") + + helper: WebAuthnHelper = WebAuthnCredential.get_webauthn_helper(request) + user_entity = helper.get_user_entity(request.user) + rp_id = helper.get_relying_party_domain() + + # Convert all credential ids to base64url-encoded strings, as is needed by + # the WebAuthn API + credential_ids = [ + bytes_to_base64url(descriptor.id) + for descriptor in WebAuthnCredential.get_credential_descriptors_for_user( + request.user + ) + ] + + # The data the client-side script uses to signal the browser + context["configuration"] = { + "rpId": rp_id, + "userId": bytes_to_base64url(user_entity.id), + "name": user_entity.name, + "displayName": user_entity.display_name, + "credentialIds": credential_ids, + } + return context diff --git a/src/django_otp_webauthn/utils.py b/src/django_otp_webauthn/utils.py index 748ae7a..8a19876 100644 --- a/src/django_otp_webauthn/utils.py +++ b/src/django_otp_webauthn/utils.py @@ -5,6 +5,7 @@ from django.apps import apps from django.core.exceptions import ImproperlyConfigured +from django.http import HttpRequest from django.urls import reverse from webauthn.helpers import exceptions as pywebauthn_exceptions @@ -149,3 +150,16 @@ def get_attestation_model_string() -> str: """Returns the string representation of the WebAuthnAttestation model that is active in this project.""" return app_settings.OTP_WEBAUTHN_ATTESTATION_MODEL + + +def request_user_details_sync(request: HttpRequest) -> None: + """Causes the `{% render_otp_webauthn_sync_signals_scripts %}` template tag + to render a script that calls the `PublicKeyCredential.signalCurrentUserDetails` + and `PublicKeyCredential.signalAllAcceptedCredentials` browser apis on the + next page load. + + This ensures the users' browser is made aware of changes to user details and + credentials. + """ + request.session["otp_webauthn_sync_needed"] = True + request.session.save() diff --git a/tests/unit/test_templatetags.py b/tests/unit/test_templatetags.py index 432e999..fc50ebd 100644 --- a/tests/unit/test_templatetags.py +++ b/tests/unit/test_templatetags.py @@ -1,5 +1,6 @@ import json +import pytest from bs4 import BeautifulSoup from django.shortcuts import resolve_url from django.template import Context, Template @@ -8,6 +9,8 @@ from django_otp_webauthn.templatetags.otp_webauthn import ( get_configuration, ) +from django_otp_webauthn.utils import request_user_details_sync +from tests.factories import WebAuthnCredentialFactory, WebAuthnUserHandleFactory def test_get_configuration__defaults(rf): @@ -116,3 +119,75 @@ def test_render_otp_webauthn_auth_scripts__no_passwordless(rf, settings): assert soup.select_one(f'script[src="{i18n_url}"]') assert soup.select_one('script[src$="otp_webauthn_auth.js"]') + + +def test_render_otp_webauthn_sync_signals_scripts__not_authenticated(client): + """Test that the sync signals scripts render nothing for anonymous users, even + if the sync needed flag is set.""" + request = client.request().wsgi_request + # Set the sync needed flag in the session + request_user_details_sync(request) + + context = Context({"request": request}) + template = Template( + "{% load otp_webauthn %}{% render_otp_webauthn_sync_signals_scripts %}" + ) + rendered = template.render(context) + # User is not authenticated, so we stay silent and render + # nothing - even though the sync needed flag is set. + assert rendered.strip() == "" + + +@pytest.mark.django_db +def test_render_otp_webauthn_sync_signals_scripts__authenticated( + client, user, settings +): + """Test that the sync signals scripts render correctly for authenticated users + when the sync needed flag is set.""" + settings.OTP_WEBAUTHN_RP_ID = "testserver" + user.username = "testuser" + user.first_name = "Test" + user.last_name = "User" + user.save() + + handle = WebAuthnUserHandleFactory(user=user) + credential1 = WebAuthnCredentialFactory(user=user) + credential2_unconfirmed = WebAuthnCredentialFactory(user=user, confirmed=False) + credential3_other_user = WebAuthnCredentialFactory() + + client.force_login(user) + request = client.request().wsgi_request + # Set the sync needed flag in the session + request_user_details_sync(request) + + context = Context({"request": request}) + template = Template( + "{% load otp_webauthn %}{% render_otp_webauthn_sync_signals_scripts %}" + ) + rendered = template.render(context) + soup = BeautifulSoup(rendered, "html.parser") + config_el = soup.select_one("script[id=otp_webauthn_sync_signals_config]") + assert config_el + config = json.loads(config_el.text) + assert config["rpId"] == "testserver" + assert config["userId"] == handle.handle_base64url + assert config["name"] == "testuser" + assert config["displayName"] == "Test User (testuser)" + + assert len(config["credentialIds"]) == 2 + + assert credential1.credential_id_base64url in config["credentialIds"] + # Even if (temporarily) unconfirmed, the credential should be included. + # It might become confirmed again later, so don't cause its removal just yet. + assert credential2_unconfirmed.credential_id_base64url in config["credentialIds"] + + # Definitely do not include credentials of other users + assert credential3_other_user.credential_id_base64url not in config["credentialIds"] + + # But if we render again, the flag should have been consumed and we stay + # silent this time + assert "otp_webauthn_sync_needed" not in request.session + + rendered = template.render(context) + # We did not set the sync needed flag, so we stay silent and render nothing + assert rendered.strip() == "" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 889a6d0..f131be0 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -9,6 +9,7 @@ get_attestation_model_string, get_credential_model, get_credential_model_string, + request_user_details_sync, rewrite_exceptions, ) @@ -167,3 +168,15 @@ def test_rewrite_exceptions_uncaught_exception(): with pytest.raises(Exception): # noqa: B017, PT011 with rewrite_exceptions(): raise Exception() + + +def test_request_user_details_sync(client): + """Test that the ``utils.request_user_details_sync`` function sets the sync needed flag in the session.""" + request = client.request().wsgi_request + + assert "otp_webauthn_sync_needed" not in request.session + + request_user_details_sync(request) + + assert "otp_webauthn_sync_needed" in request.session + assert request.session["otp_webauthn_sync_needed"] is True From 51e23d63f29c8cc867ab9fae2e9ab102518fb3ff Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:49:06 +0200 Subject: [PATCH 07/21] Add properties to base64 encode handle and credential id --- src/django_otp_webauthn/models.py | 11 +++++++++- tests/unit/test_models.py | 35 +++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/django_otp_webauthn/models.py b/src/django_otp_webauthn/models.py index ad25767..0daa5af 100644 --- a/src/django_otp_webauthn/models.py +++ b/src/django_otp_webauthn/models.py @@ -13,7 +13,7 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from django_otp.models import Device, DeviceManager, TimestampMixin -from webauthn.helpers import parse_attestation_object +from webauthn.helpers import bytes_to_base64url, parse_attestation_object from webauthn.helpers.structs import ( AttestationObject, AuthenticatorTransport, @@ -383,6 +383,11 @@ def get_webauthn_helper(cls, request: HttpRequest): helper = import_string(app_settings.OTP_WEBAUTHN_HELPER_CLASS) return helper(request=request) + @property + def credential_id_base64url(self) -> str: + """Return the base64url-encoded credential id.""" + return bytes_to_base64url(self.credential_id) + class WebAuthnCredential(AbstractWebAuthnCredential): """A OTP device that validates against a user's credential. @@ -454,6 +459,10 @@ def save(self, *args, **kwargs): def handle(self) -> bytes: return bytes.fromhex(self.handle_hex) + @property + def handle_base64url(self) -> str: + return bytes_to_base64url(self.handle) + @classmethod def get_handle_for_user(cls, user: AbstractBaseUser) -> bytes: """Return the user handle for the given user.""" diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index e6d9ea6..2cc71d7 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -6,6 +6,7 @@ from django.db import IntegrityError from django.test.utils import isolate_apps from django.utils import timezone +from webauthn.helpers import bytes_to_base64url from webauthn.helpers.structs import ( AttestationObject, AuthenticatorTransport, @@ -128,8 +129,19 @@ def test_credentials_are_unique(): @pytest.mark.django_db -def test_get_by_credential_id(django_assert_num_queries): - """Test that the get_by_credential_id method works.""" +def test_credential_credential_id_base64url(): + """Test that the credential_id_base64url property works.""" + credential_id = b"credential_id" + expected_base64url = "Y3JlZGVudGlhbF9pZA" + + credential = WebAuthnCredentialFactory(credential_id=credential_id) + + assert credential.credential_id_base64url == expected_base64url + + +@pytest.mark.django_db +def test_credential_get_by_credential_id(django_assert_num_queries): + """Test that WebAuthnCredential.get_by_credential_id method works.""" credential_id = b"credential_id" cred1 = WebAuthnCredentialFactory(credential_id=credential_id) @@ -253,6 +265,25 @@ def test_user_handle_generate_handle_hex(user_handle_model): assert len(handle) == 64 +@pytest.mark.django_db +def test_user_handle_handle_property(user_handle): + """Test that the handle property works.""" + handle_bytes = user_handle.handle + + assert isinstance(handle_bytes, bytes) + assert handle_bytes == bytes.fromhex(user_handle.handle_hex) + + +@pytest.mark.django_db +def test_user_handle_handle_base64url_property(user_handle): + """Test that the handle_base64url property works.""" + expected_base64url = bytes_to_base64url(bytes.fromhex(user_handle.handle_hex)) + handle_base64url = user_handle.handle_base64url + + assert isinstance(handle_base64url, str) + assert handle_base64url == expected_base64url + + @pytest.mark.django_db def test_user_handle__str__(user_handle): """Test that the __str__ method works.""" From d0365af9df8460cdcf051544a79eb95a83248c82 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:49:47 +0200 Subject: [PATCH 08/21] Fix selector bug --- tests/e2e/fixtures.py | 2 +- tests/e2e/test_credential_registration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/fixtures.py b/tests/e2e/fixtures.py index 345ce5d..9a69de8 100644 --- a/tests/e2e/fixtures.py +++ b/tests/e2e/fixtures.py @@ -13,7 +13,7 @@ class StatusEnum(enum.StrEnum): UNKNOWN_ERROR = "unknown-error" STATE_ERROR = "state-error" SECURITY_ERROR = "security-error" - GET_OPTIONS_FAILED = ("get-options-failed",) + GET_OPTIONS_FAILED = "get-options-failed" ABORTED = "aborted" NOT_ALLOWED_OR_ABORTED = "not-allowed-or-aborted" SERVER_ERROR = "server-error" diff --git a/tests/e2e/test_credential_registration.py b/tests/e2e/test_credential_registration.py index 82083dc..afbb653 100644 --- a/tests/e2e/test_credential_registration.py +++ b/tests/e2e/test_credential_registration.py @@ -252,7 +252,7 @@ def test_register_credential__fail_bad_rpid( register_button.click() page.wait_for_selector( - f"#passkey-register-status-message[data-status-enum='{StatusEnum.SECURITY_ERROR}']", + f"#passkey-register-status-message[data-status-enum='{StatusEnum.SECURITY_ERROR.value}']", timeout=2000, ) # Did the right events fire? From 5ad9d80ebbe0b87e44648deb69c9773abe6eb629 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:50:33 +0200 Subject: [PATCH 09/21] Add `playwright_manipulate_session` fixture And update `playwright_force_login` to use it. --- tests/e2e/conftest.py | 60 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index c43eeee..c427502 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -128,22 +128,68 @@ def _get_credential(authenticator_id: str, credential: VirtualCredential): @pytest.fixture -def playwright_force_login(live_server, context): - """Fixture that forces the given user to be logged in by manipulating the session cookie.""" +def playwright_manipulate_session(live_server, context): + """Fixture that allows direct manipulation of the session. - def _playwright_force_login(user): - login_helper = DjangoTestClient() - login_helper.force_login(user) + Usage: + def my_pytest_playwright_testcase(live_server, page, playwright_manipulate_session): + # Update or add a specific key: + playwright_manipulate_session(lambda session: session.update({"some_key": "some_value"})) + + # Remove a specific key: + playwright_manipulate_session(lambda session: session.pop("some_key", None)) + + # Clear the session entirely: + playwright_manipulate_session(lambda session: session.clear()) + + # Navigate the live server as normal, with the updated session: + page.goto(live_server.url) + """ + + def _playwright_manipulate_session(modify_func): + # Get the session id cookie from the browser context + session_cookie_value = None + for cookie in context.cookies(): + if cookie["name"] == "sessionid": + session_cookie_value = cookie["value"] + break + + # Instantiate a Django test client to gain access to the SessionStore + # and load the session if we already have one + client = DjangoTestClient() + if session_cookie_value: + client.cookies["sessionid"] = session_cookie_value + + session = client.session + + # let the caller modify the session + modify_func(session) + session.save() + # Update the browser context with the new session id cookie context.add_cookies( [ { "url": live_server.url, "name": "sessionid", - "value": login_helper.cookies["sessionid"].value, + "value": session.session_key, } ] ) - return user + + return _playwright_manipulate_session + + +@pytest.fixture +def playwright_force_login(live_server, context, playwright_manipulate_session): + """Fixture that forces the given user to be logged in by manipulating the session cookie.""" + + def _playwright_force_login(user): + login_helper = DjangoTestClient() + login_helper.force_login(user) + + playwright_manipulate_session( + lambda session: session.update(login_helper.session.items()) + ) return _playwright_force_login From ac0a496db262c5c580e25456aaa406b51948e9d1 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:51:08 +0200 Subject: [PATCH 10/21] Reimplement `wait_for_console_message` fixture To support waiting for multiple messages at the same time. This code was 90% generated by Copilot. --- tests/e2e/conftest.py | 82 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index c427502..7f6029d 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -81,23 +81,83 @@ def _return(): @pytest.fixture def wait_for_console_message(page): - """Returns a function that blocks until a certain console message has been posted.""" + """Returns a function that blocks until a certain console message has been posted. - def _wait_for_console_message(message_text_regex: str, *, level="log"): - future = FutureWrapper() + Only saves the first matching message. You can use this fixture multiple times + in a test to wait for multiple different messages. - def _handle_console_message(msg): - if msg.type == level and re.match(message_text_regex, msg.text): - future.set_result(msg) + Args: + message_text_regex: A regex string to match against the console message text. + level: The console message level to match against (e.g. "log", "error", etc.) - page.on("console", _handle_console_message) + Returns: + A function that, when called, blocks until the console message is seen, and then returns the playwright ConsoleMessage object. + See: https://playwright.dev/python/docs/api/class-consolemessage - def _return(): - return future.get_result() + Usage: + def my_testcase(live_server, page, wait_for_console_message): + # Set up the listener to capture the console message + await_message = wait_for_console_message(r"hello", level="info") - return _return + page.goto(live_server.url) + # Do stuff that triggers the console message + page.evaluate("console.log('hello', 42, { foo: 'bar' })") + + # Wait for and get the console message. This will block until the message is seen, or raise a timeout error. + msg = await_message() + + # Now you can inspect the msg object as needed + msg.args[0].json_value() # hello + msg.args[1].json_value() # 42 + """ - return _wait_for_console_message + waiters: list[dict] = [] + buffer: list = [] + + def _on_console(msg): + # store raw messages so later-created waiters can match them too + buffer.append(msg) + # satisfy any outstanding waiters + for waiter in list(waiters): + try: + if not waiter["future"].done() and waiter["regex"].search(msg.text): + waiter["future"].set_result(msg) + except Exception: # noqa: S110, BLE001 - yes, do catch naked exceptions and don't log them + # guard handler from raising (handlers should be best-effort) + pass + + # register the listener immediately so we don't miss early messages + page.on("console", _on_console) + + def _factory(pattern, timeout: int = 5000, level: str | None = None): + regex = re.compile(pattern) if isinstance(pattern, (str, bytes)) else pattern + # If we've already seen a matching message, return it immediately. + for msg in buffer: + if regex.search(msg.text) and (level is None or msg.type == level): + + def _immediate(msg=msg): + return msg + + return _immediate + + future = Future() + waiter = {"regex": regex, "future": future} + waiters.append(waiter) + + def _waiter(): + try: + # concurrent.futures.Future expects seconds, but our testing convention is milliseconds + sec = timeout / 1000.0 if timeout is not None else None + return future.result(timeout=sec) + finally: + try: + waiters.remove(waiter) + except ValueError: + pass + + return _waiter + + return _factory @pytest.fixture From d40806990bf6a7731221f628a9b09248d2ff6fa9 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Fri, 24 Oct 2025 16:52:11 +0200 Subject: [PATCH 11/21] Add test for checking WebAuthn signal apis are called --- tests/e2e/test_webauthn_signals.py | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/e2e/test_webauthn_signals.py diff --git a/tests/e2e/test_webauthn_signals.py b/tests/e2e/test_webauthn_signals.py new file mode 100644 index 0000000..d1ff751 --- /dev/null +++ b/tests/e2e/test_webauthn_signals.py @@ -0,0 +1,69 @@ +import pytest +from playwright.sync_api import expect + +from tests.factories import WebAuthnCredentialFactory + + +def test_webauthn_signals_triggered( + live_server, + django_db_serialized_rollback, + page, + playwright_force_login, + playwright_manipulate_session, + user, + wait_for_console_message, +): + """Verify the ``PublicKeyCredential.signalCurrentUserDetails`` and + ``PublicKeyCredential.signalAllAcceptedCredentials`` browser signals are + triggered. + """ + + playwright_force_login(user) + playwright_manipulate_session( + lambda session: session.update({"otp_webauthn_sync_needed": True}) + ) + + # Create some credentials for the user + WebAuthnCredentialFactory(user=user) + WebAuthnCredentialFactory(user=user) + + # Set up signal waiters + await_signal_accepted_credentials = wait_for_console_message( + r"Signaled accepted credentials to the browser." + ) + await_signal_user_details = wait_for_console_message( + r"Signaled current user details to the browser." + ) + + page.goto(live_server.url) + + # Double check that PublicKeyCredential.signalCurrentUserDetails and + # PublicKeyCredential.signalAllAcceptedCredentials apis are available in this browser + # otherwise the result won't be meaningful. + if not page.evaluate("() => 'signalCurrentUserDetails' in PublicKeyCredential"): + pytest.skip( + "PublicKeyCredential.signalCurrentUserDetails does not exist in this browser, cannot test this feature!", + ) + if not page.evaluate("() => 'signalAllAcceptedCredentials' in PublicKeyCredential"): + pytest.skip( + "PublicKeyCredential.signalAllAcceptedCredentials does not exist in this browser, cannot test this feature!", + ) + + # Wait for the signals to be sent + try: + await_signal_accepted_credentials() + except TimeoutError: + pytest.fail( + "sync_signals.ts did not post console message indicating PublicKeyCredential.signalAllAcceptedCredentials was called." + ) + try: + await_signal_user_details() + except TimeoutError: + pytest.fail( + "sync_signals.ts did not post console message indicating PublicKeyCredential.signalCurrentUserDetails was called." + ) + + # Verify that the config script tag has been removed from the DOM + expect(page.locator("script[id='otp_webauthn_sync_signals_config']")).to_have_count( + 0 + ) From 9238525623600467229cbe293da0974282e09e27 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 14:37:34 +0100 Subject: [PATCH 12/21] Add user-facing error message when security error occurs... ...during passkey verification through the button. --- client/src/auth.ts | 11 ++++ .../locale/nl/LC_MESSAGES/django.po | 54 +++++++++---------- .../locale/nl/LC_MESSAGES/djangojs.po | 10 +++- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/client/src/auth.ts b/client/src/auth.ts index 6b51ebf..4a87758 100644 --- a/client/src/auth.ts +++ b/client/src/auth.ts @@ -290,6 +290,17 @@ import { status: gettext("Verification canceled or not allowed."), }); break; + case "SecurityError": + await setPasskeyVerifyState({ + buttonDisabled: false, + buttonLabel, + requestFocus: true, + statusEnum: StatusEnum.SECURITY_ERROR, + status: gettext( + "Passkey authentication failed. A technical problem prevents the verification process for beginning. Please try another method.", + ), + }); + break; default: await setPasskeyVerifyState({ buttonDisabled: false, diff --git a/src/django_otp_webauthn/locale/nl/LC_MESSAGES/django.po b/src/django_otp_webauthn/locale/nl/LC_MESSAGES/django.po index 5a2c7eb..cd82b6c 100644 --- a/src/django_otp_webauthn/locale/nl/LC_MESSAGES/django.po +++ b/src/django_otp_webauthn/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-27 21:44+0000\n" +"POT-Creation-Date: 2025-11-22 13:46+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,19 +18,19 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/django_otp_webauthn/admin.py:41 +#: src/django_otp_webauthn/admin.py:38 msgid "COSE public key" msgstr "Publieke COSE-sleutel" -#: src/django_otp_webauthn/admin.py:63 +#: src/django_otp_webauthn/admin.py:62 msgid "Identity" msgstr "Identiteit" -#: src/django_otp_webauthn/admin.py:69 +#: src/django_otp_webauthn/admin.py:68 msgid "Meta" msgstr "Meta-gegevens" -#: src/django_otp_webauthn/admin.py:75 +#: src/django_otp_webauthn/admin.py:74 msgid "WebAuthn credential data" msgstr "WebAuthn-gegevens" @@ -68,26 +68,26 @@ msgstr "" "De Passkey die u probeert te gebruiken is niet gevonden. Misschien is deze " "verwijderd?" -#: src/django_otp_webauthn/models.py:79 -msgid "WebAuthn attestation" -msgstr "WebAuthn attestatie" - -#: src/django_otp_webauthn/models.py:80 -msgid "WebAuthn attestations" -msgstr "WebAuthn attestaties" - -#: src/django_otp_webauthn/models.py:96 +#: src/django_otp_webauthn/models.py:86 msgid "format" msgstr "formaat" -#: src/django_otp_webauthn/models.py:100 +#: src/django_otp_webauthn/models.py:90 msgid "data" msgstr "data" -#: src/django_otp_webauthn/models.py:104 +#: src/django_otp_webauthn/models.py:94 msgid "client data JSON" msgstr "client data JSON" +#: src/django_otp_webauthn/models.py:106 +msgid "WebAuthn attestation" +msgstr "WebAuthn attestatie" + +#: src/django_otp_webauthn/models.py:107 +msgid "WebAuthn attestations" +msgstr "WebAuthn attestaties" + #: src/django_otp_webauthn/models.py:148 msgid "WebAuthn credential" msgstr "WebAuthn credential" @@ -140,22 +140,22 @@ msgstr "gehashte credential id" msgid "discoverable" msgstr "zichtbaar" -#: src/django_otp_webauthn/models.py:404 +#: src/django_otp_webauthn/models.py:407 msgid "credential" msgstr "credential" -#: src/django_otp_webauthn/models.py:429 -msgid "WebAuthn user handle" -msgstr "WebAuthn user handle" - -#: src/django_otp_webauthn/models.py:430 -msgid "WebAuthn user handles" -msgstr "WebAuthn user handles" - -#: src/django_otp_webauthn/models.py:437 +#: src/django_otp_webauthn/models.py:433 msgid "handle hex" msgstr "handle hex" -#: src/django_otp_webauthn/models.py:447 +#: src/django_otp_webauthn/models.py:443 msgid "user" msgstr "gebruiker" + +#: src/django_otp_webauthn/models.py:447 +msgid "WebAuthn user handle" +msgstr "WebAuthn user handle" + +#: src/django_otp_webauthn/models.py:448 +msgid "WebAuthn user handles" +msgstr "WebAuthn user handles" diff --git a/src/django_otp_webauthn/locale/nl/LC_MESSAGES/djangojs.po b/src/django_otp_webauthn/locale/nl/LC_MESSAGES/djangojs.po index e4fab57..d3874e1 100644 --- a/src/django_otp_webauthn/locale/nl/LC_MESSAGES/djangojs.po +++ b/src/django_otp_webauthn/locale/nl/LC_MESSAGES/djangojs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-11 17:52+0200\n" +"POT-Creation-Date: 2025-11-22 13:46+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -40,6 +40,14 @@ msgstr "Verificatie afgebroken." msgid "Verification canceled or not allowed." msgstr "Verificatie geannuleerd of niet toegestaan." +msgid "" +"Passkey authentication failed. A technical problem prevents the verification " +"process for beginning. Please try another method." +msgstr "" +"Inloggen met Passkey mislukt. Er is een technisch probleem opgetreden " +"waardoor het proces niet kan worden gestart. Probeer een andere " +"authenticatiemethode." + msgid "Verification failed. An unknown error occurred." msgstr "Verificatie mislukt. Er is een onbekende fout opgetreden." From 7be1db9d2bb90f79bdc7b89ea3f5fb20423bc72f Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 15:05:44 +0100 Subject: [PATCH 13/21] Avoid final newlines in templatetag templates Don't need no noise. --- .editorconfig | 4 ++++ .pre-commit-config.yaml | 6 ++++++ .../templates/django_otp_webauthn/auth_scripts.html | 2 +- .../templates/django_otp_webauthn/register_scripts.html | 2 +- .../templates/django_otp_webauthn/sync_signals_scripts.html | 2 +- tests/unit/test_templatetags.py | 4 ++-- 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0018f03..f93062d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,9 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 +[src/django_otp_webauthn/templates/*.html] +# Final newlines don't have to end up in the HTML output of end users +insert_final_newline = false + [*.md] trim_trailing_whitespace = false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a3747d..2564fec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,12 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer + exclude: | + (?x)^( + src/django_otp_webauthn/templates/django_otp_webauthn/auth_scripts.html| + src/django_otp_webauthn/templates/django_otp_webauthn/register_scripts.html| + src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html + )$ - id: check-added-large-files - id: check-case-conflict - id: check-json diff --git a/src/django_otp_webauthn/templates/django_otp_webauthn/auth_scripts.html b/src/django_otp_webauthn/templates/django_otp_webauthn/auth_scripts.html index 8a3a52e..fcc1dce 100644 --- a/src/django_otp_webauthn/templates/django_otp_webauthn/auth_scripts.html +++ b/src/django_otp_webauthn/templates/django_otp_webauthn/auth_scripts.html @@ -2,4 +2,4 @@ {{ configuration|json_script:"otp_webauthn_config"}} -{% endspaceless %} +{% endspaceless %} \ No newline at end of file diff --git a/src/django_otp_webauthn/templates/django_otp_webauthn/register_scripts.html b/src/django_otp_webauthn/templates/django_otp_webauthn/register_scripts.html index 102f6ad..a8548a1 100644 --- a/src/django_otp_webauthn/templates/django_otp_webauthn/register_scripts.html +++ b/src/django_otp_webauthn/templates/django_otp_webauthn/register_scripts.html @@ -2,4 +2,4 @@ {{ configuration|json_script:"otp_webauthn_config"}} -{% endspaceless %} +{% endspaceless %} \ No newline at end of file diff --git a/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html b/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html index de3490a..f19ac70 100644 --- a/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html +++ b/src/django_otp_webauthn/templates/django_otp_webauthn/sync_signals_scripts.html @@ -3,4 +3,4 @@ {{ configuration|json_script:"otp_webauthn_sync_signals_config"}} {% endif %} -{% endspaceless %} +{% endspaceless %} \ No newline at end of file diff --git a/tests/unit/test_templatetags.py b/tests/unit/test_templatetags.py index fc50ebd..335b00e 100644 --- a/tests/unit/test_templatetags.py +++ b/tests/unit/test_templatetags.py @@ -135,7 +135,7 @@ def test_render_otp_webauthn_sync_signals_scripts__not_authenticated(client): rendered = template.render(context) # User is not authenticated, so we stay silent and render # nothing - even though the sync needed flag is set. - assert rendered.strip() == "" + assert rendered == "" @pytest.mark.django_db @@ -190,4 +190,4 @@ def test_render_otp_webauthn_sync_signals_scripts__authenticated( rendered = template.render(context) # We did not set the sync needed flag, so we stay silent and render nothing - assert rendered.strip() == "" + assert rendered == "" From 10881b3bc5349d4c184dd6b5bc5150da4da8fc07 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 15:32:59 +0100 Subject: [PATCH 14/21] fixup! Add support for signaling current user details and credentials --- src/django_otp_webauthn/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/django_otp_webauthn/utils.py b/src/django_otp_webauthn/utils.py index 8a19876..cca2adc 100644 --- a/src/django_otp_webauthn/utils.py +++ b/src/django_otp_webauthn/utils.py @@ -153,8 +153,11 @@ def get_attestation_model_string() -> str: def request_user_details_sync(request: HttpRequest) -> None: - """Causes the `{% render_otp_webauthn_sync_signals_scripts %}` template tag - to render a script that calls the `PublicKeyCredential.signalCurrentUserDetails` + """Marks the current session as needing user details and accepted credentials + synchronization with the browser. + + The implementation will cause the `{% render_otp_webauthn_sync_signals_scripts %}` + template tag to render a script that calls the `PublicKeyCredential.signalCurrentUserDetails` and `PublicKeyCredential.signalAllAcceptedCredentials` browser apis on the next page load. From 36789b45f90dd65ee91d12e976e054ba5df207c1 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 15:34:27 +0100 Subject: [PATCH 15/21] Request user details sync after Passkey login and registration --- src/django_otp_webauthn/views.py | 9 ++++++++- tests/integration/test_views_authentication.py | 8 ++++++++ tests/integration/test_views_registration.py | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/django_otp_webauthn/views.py b/src/django_otp_webauthn/views.py index 173f08b..42f5b04 100644 --- a/src/django_otp_webauthn/views.py +++ b/src/django_otp_webauthn/views.py @@ -21,7 +21,11 @@ from django_otp_webauthn import exceptions from django_otp_webauthn.models import AbstractWebAuthnCredential from django_otp_webauthn.settings import app_settings -from django_otp_webauthn.utils import get_credential_model, rewrite_exceptions +from django_otp_webauthn.utils import ( + get_credential_model, + request_user_details_sync, + rewrite_exceptions, +) WebAuthnCredential = get_credential_model() User = get_user_model() @@ -153,6 +157,8 @@ def post(self, *args, **kwargs): # change that indicator. if not self.request.user.is_verified(): otp_login(self.request, device) + + request_user_details_sync(self.request) return Response(data={"id": device.pk}, content_type="application/json") @@ -245,6 +251,7 @@ def complete_auth(self, device: AbstractWebAuthnCredential) -> AbstractBaseUser: # Mark the user as having passed verification otp_login(self.request, device) + request_user_details_sync(self.request) success_url_allowed_hosts = set() diff --git a/tests/integration/test_views_authentication.py b/tests/integration/test_views_authentication.py index 78d9e9b..346490e 100644 --- a/tests/integration/test_views_authentication.py +++ b/tests/integration/test_views_authentication.py @@ -212,6 +212,8 @@ def test_authentication_complete__anonymous_user_passwordless_login_allowed( assert ( session["_auth_user_backend"] == "django_otp_webauthn.backends.WebAuthnBackend" ) + # Signaled that user details sync is needed + assert session["otp_webauthn_sync_needed"] is True @pytest.mark.django_db @@ -235,6 +237,8 @@ def test_authentication_complete__verify_existing_user(api_client, settings, use session = api_client.session assert "otp_webauthn_authentication_state" not in session assert session["otp_device_id"] == credential.persistent_id + # Signaled that user details sync is needed + assert session["otp_webauthn_sync_needed"] is True @pytest.mark.django_db @@ -258,6 +262,8 @@ def test_authentication_complete_device_usable__unconfirmed(api_client, user): session = api_client.session assert "otp_webauthn_authentication_state" not in session assert "otp_device_id" not in session + # No user details sync should be requested + assert "otp_webauthn_sync_needed" not in session @pytest.mark.django_db @@ -285,6 +291,8 @@ def test_authentication_complete_device_usable__user_disabled( session = api_client.session assert "otp_webauthn_authentication_state" not in session assert "otp_device_id" not in session + # No user details sync should be requested + assert "otp_webauthn_sync_needed" not in session @pytest.mark.django_db diff --git a/tests/integration/test_views_registration.py b/tests/integration/test_views_registration.py index 8ab2029..c55c661 100644 --- a/tests/integration/test_views_registration.py +++ b/tests/integration/test_views_registration.py @@ -241,6 +241,9 @@ def test_registration_complete__valid_response_but_already_verified( assert cred.persistent_id != credential.persistent_id assert api_client.session["otp_device_id"] == credential.persistent_id + # Signaled that user details sync is needed + assert api_client.session["otp_webauthn_sync_needed"] is True + @pytest.mark.django_db def test_registration_complete__valid_response(api_client, user, credential_model): @@ -279,3 +282,6 @@ def test_registration_complete__valid_response(api_client, user, credential_mode # The user session wasn't 2FA verified before, so now it should be assert api_client.session["otp_device_id"] == cred.persistent_id + + # Signaled that user details sync is needed + assert api_client.session["otp_webauthn_sync_needed"] is True From da39aeace8d18f8f1ee018c4c92bf50f36959616 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 16:57:54 +0100 Subject: [PATCH 16/21] Add how-to documentation about using `render_otp_webauthn_sync_signals_scripts` --- docs/how_to_guides/index.rst | 5 + .../keeping_passkeys_in_sync.rst | 140 ++++++++++++++++++ docs/wordlist.txt | 20 ++- 3 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 docs/how_to_guides/keeping_passkeys_in_sync.rst diff --git a/docs/how_to_guides/index.rst b/docs/how_to_guides/index.rst index 08cd96e..a9b38bc 100644 --- a/docs/how_to_guides/index.rst +++ b/docs/how_to_guides/index.rst @@ -25,6 +25,10 @@ Here are what you will find in this section: Learn how to configure WebAuthn to work across multiple domains. For example, if your main application runs on ``https://example.com`` and you have a localized version on ``https://example.co.uk``. + .. grid-item-card:: :ref:`Keeping Passkeys up-to-date with changing user details ` + + Learn how to keep Passkey user details saved in users' browsers up-to-date when details like email or username change. + .. toctree:: :maxdepth: 2 :hidden: @@ -33,3 +37,4 @@ Here are what you will find in this section: Customize helper class Customize models Configure related origins + Keeping Passkeys up-to-date with changing user details diff --git a/docs/how_to_guides/keeping_passkeys_in_sync.rst b/docs/how_to_guides/keeping_passkeys_in_sync.rst new file mode 100644 index 0000000..023bba4 --- /dev/null +++ b/docs/how_to_guides/keeping_passkeys_in_sync.rst @@ -0,0 +1,140 @@ +.. _keeping_passkeys_in_sync: + +Keeping user details in sync with Passkeys +========================================== + +When your users make changes to their details, such as their email address or +username, these changes won't automatically be reflected in the Passkeys they +have saved in their browser. This can lead to confusion as their old email or +username may still appear during Passkey authentication. + +To ensure a smooth user experience, it's important to keep the user details +associated with Passkeys up to date. Django OTP WebAuthn has a template tag that +can help with this by calling the right browser APIs to update the stored +Passkeys for you. + + +.. _the_render_otp_webauthn_sync_signals_scripts_template_tag: + +The ``render_otp_webauthn_sync_signals_scripts`` template tag +------------------------------------------------------------- + +This is a lightweight template tag that you can add to your base template. It +looks for a key in the session that indicates that user details have changed, +and if so, it renders the necessary JavaScript to update the stored Passkeys in +the user's browser. If no syncing is needed, it outputs nothing so it won't +unnecessarily bloat your pages. + +To use this template tag, you would add it near the bottom of your base +template, just before the closing ```` tag. This way it won't block the +initial page load, but will still be executed when the page is fully loaded. + +.. code-block:: html + + + {% load otp_webauthn %} + ... + + ... + + ... + ... + {% render_otp_webauthn_sync_signals_scripts %} + + + +How it works +------------ + +When a user authenticates using a Passkey or registers a new Passkey, the +default authentication and registration views will automatically call +``django_otp_webauthn.utils.request_user_details_sync`` after successful +registration or authentication respectively. This function sets a flag in the +user's session indicating that their details need to be synchronized. This is +the official way to request a user details sync from Django OTP WebAuthn. + +On the next page load, when templates are being rendered, the template tag sees +this flag and outputs JavaScript code that uses the appropriate WebAuthn API to +update the stored Passkeys with the latest user details. After the +synchronization is complete, the flag is cleared from the session to prevent +rendering unnecessary the JavaScript on subsequent page loads. + +Removing deleted Passkeys from the browser +------------------------------------------ + +If a user removes a Passkey from your application – most likely through an +account settings page that you have created – this removal won't automatically +be reflected in the Passkeys stored in their browser. Meaning the Passkey will +still show up the next time the user tries to authenticate. + +To deal with this appropriately, you should call the +``request_user_details_sync`` utility function after a Passkey is removed. This +will ensure that the next time the user loads a page, their browser will be +informed about the deleted Passkey and can remove it from storage. Sometimes +this is associated with a prompt to the user to confirm the removal, depending +on the browser's implementation. + +In the event that the user does try to use a removed Passkey during +authentication, the browser will automatically be informed that the Passkey is +no longer valid through the `PublicKeyCredential.signalUnknownCredential +`_ +WebAuthn API. + + +How to trigger user details sync +-------------------------------- + +To trigger the user details synchronization process, you need to call the +``request_user_details_sync`` utility function whenever a user's details are +updated. For example, if you have a view that allows users to update their email +address or username, you would add a call to this function after the update is +successful. This will ensure that the next time the user loads a page, their +Passkeys will be updated with the new details. + +.. code-block:: py + + # your_app/views.py + from django_otp_webauthn.utils import request_user_details_sync + + def update_user_details(request): + if request.method == "POST": + # Assume we have a form that updates user details + form = UserDetailsForm(request.POST, instance=request.user) + if form.is_valid(): + form.save() + # Request user details sync after updating details + request_user_details_sync(request) + # Redirect or render success response + return redirect("profile") + else: + form = UserDetailsForm(instance=request.user) + return render(request, "update_user_details.html", {"form": form}) + +By following these steps, you can ensure that your users' Passkeys remain +up to date with their latest details. + +You won't see any visible messages or indicators when the synchronization occurs, as +it happens silently in the background. However, you can check your browser's +console for any errors or logs related to the synchronization process if needed. + +How does this work from a technical perspective? +------------------------------------------------ + +The ``render_otp_webauthn_sync_signals_scripts`` template tag is a convenience +wrapper that ends up calling the +``PublicKeyCredential.signalAllAcceptedCredentials`` and +``PublicKeyCredential.signalCurrentUserDetails`` WebAuthn browser APIs. It +automatically retrieves a list of currently registered credentials and the +current user details in the format these APIs expect. + +For more information about these APIs, refer to the following resources: + +- `PublicKeyCredential.signalCurrentUserDetails `_ +- `PublicKeyCredential.signalAllAcceptedCredentials `_ + +.. note:: + + As of November 2025, these APIs are still relatively new and don't enjoy + broad support from all browsers. Please see `Web authentication signal + methods on caniuse.com `_ for the + most up-to-date browser support information. diff --git a/docs/wordlist.txt b/docs/wordlist.txt index beac3c0..3683e96 100644 --- a/docs/wordlist.txt +++ b/docs/wordlist.txt @@ -1,5 +1,6 @@ AbstractWebAuthnAttestation AbstractWebAuthnCredential +APIs auth authenticator authenticator's @@ -13,42 +14,47 @@ BeginCredentialRegistrationView biometric biometrics Caddy +caniuse ccTLD CompleteCredentialAuthenticationView CompleteCredentialRegistrationView cryptographic django -Frontend frontend +Frontend Furo gettext github -http htmlcov +http HTTPS js JSON localhost OneToOneField +otp OTP OTPMiddleware -otp passwordless pradyunsg pre -PyPI +PublicKeyCredential py +PyPI Quickstart +residentKey +reStructuredText rpID rpIDs -reStructuredText -residentKey +signalAllAcceptedCredentials +signalCurrentUserDetails +signalUnknownCredential Stormbase subclasses subclassing untrusted -WebAuthn webauthn +WebAuthn WebAuthn's WebAuthnCredential WebAuthnUserHandle From 34feb4933d3e50b0b2a19f24444b30f478a830f4 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 16:58:48 +0100 Subject: [PATCH 17/21] Link to next steps at end of quickstart --- docs/getting_started/quickstart.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/getting_started/quickstart.rst b/docs/getting_started/quickstart.rst index 669e545..eb9de8d 100644 --- a/docs/getting_started/quickstart.rst +++ b/docs/getting_started/quickstart.rst @@ -248,3 +248,11 @@ Once you’ve done this, you will see the following on your login page: * a **Register Passkey** button on the login page * a **Login using a Passkey** button on the login page + +Next steps +---------- + +Now that you have Django OTP WebAuthn set up, you can explore additional features such as: + +* :ref:`keeping_passkeys_in_sync`, to improve your users' experience. +* :ref:`configure_related_origins`, for when your application is active on multiple domains and you want to share Passkeys across them. From 39f31538db9d002c8ca606a104da89383cf2e857 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 22 Nov 2025 17:26:00 +0100 Subject: [PATCH 18/21] Update changelog entry for #96 --- CHANGELOG.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9890718..b51fb16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **New feature (experimental):** the browser will now be signaled to remove an unknown credential after a failed authentication attempt. This is controlled by the new `OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL` setting, which defaults to `True`. If set to `False`, the browser will not be signaled. - - The purpose of this is to improve user experience by removing credentials that are no longer valid from the users' device, stopping the user from being prompted to use this credential in the future. - - The exact response of browsers to the signal varies, most browsers tested appear to ignore this signal and thus this feature has no effect. - - This uses a draft feature defined the WebAuthn L3 specification: https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#sctn-signal-methods. +- **New feature (experimental):** the browser will now be signaled to remove an unknown Passkey after a failed authentication attempt. + - The purpose of this is to improve user experience by removing Passkeys that are no longer valid from the users' device, stopping the user from being prompted to use this Passkey in the future. + - This is controlled by the new `OTP_WEBAUTHN_SIGNAL_UNKNOWN_CREDENTIAL` setting, which defaults to `True`. If set to `False`, the browser will not be signaled. - It works on recent versions of Chrome, Edge and Safari but not Firefox (as of October 2025). - Read more about the browser API used: [`PublicKeyCredential.signalUnknownCredential` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalUnknownCredential_static). - This feature is experimental because not all browsers support it properly yet. The specification is also still in draft status and may change in the future. +- **New feature (experimental)**: the `render_otp_webauthn_sync_signals_scripts` template tag has been added to allow updating user details stored in the browser when they change on the server side. + - The purpose of this is to improve user experience by keeping the user details (like display name) in sync between server and client, so that the browser can show the correct information when prompting the user to select a Passkey. + - It works on recent versions of Chrome, Edge and Safari but not Firefox (as of October 2025). + - This feature is experimental because not all browsers support it properly yet. The specification is also still in draft status and may change in the future. + - Read more about the browser APIs used: + - [`PublicKeyCredential.signalCurrentUserDetails` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalCurrentUserDetails_static) + - [`PublicKeyCredential.signalAllAcceptedCredentials` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalAllAcceptedCredentials_static) ### Changed From 3876fc14e0885cdeba02a5c6741953ebf652c09e Mon Sep 17 00:00:00 2001 From: Storm Heg Date: Tue, 25 Nov 2025 15:07:04 +0100 Subject: [PATCH 19/21] Apply Damilola's suggestions Thank you Damilola for your editing! Co-authored-by: Damilola Oladele <98895460+activus-d@users.noreply.github.com> --- docs/how_to_guides/index.rst | 2 +- .../keeping_passkeys_in_sync.rst | 92 ++++++------------- 2 files changed, 27 insertions(+), 67 deletions(-) diff --git a/docs/how_to_guides/index.rst b/docs/how_to_guides/index.rst index a9b38bc..7a39e2f 100644 --- a/docs/how_to_guides/index.rst +++ b/docs/how_to_guides/index.rst @@ -37,4 +37,4 @@ Here are what you will find in this section: Customize helper class Customize models Configure related origins - Keeping Passkeys up-to-date with changing user details + Keep passkeys up-to-date diff --git a/docs/how_to_guides/keeping_passkeys_in_sync.rst b/docs/how_to_guides/keeping_passkeys_in_sync.rst index 023bba4..01b888e 100644 --- a/docs/how_to_guides/keeping_passkeys_in_sync.rst +++ b/docs/how_to_guides/keeping_passkeys_in_sync.rst @@ -1,33 +1,21 @@ -.. _keeping_passkeys_in_sync: +.. _keep_passkeys_up_to_date: -Keeping user details in sync with Passkeys +Keep passkeys up to date with user details ========================================== -When your users make changes to their details, such as their email address or -username, these changes won't automatically be reflected in the Passkeys they -have saved in their browser. This can lead to confusion as their old email or -username may still appear during Passkey authentication. +When users update their details, such as their email address or username, the changes don't automatically reflect in their stored :term:`passkeys `. This can cause confusion because outdated information still appears during authentication. -To ensure a smooth user experience, it's important to keep the user details -associated with Passkeys up to date. Django OTP WebAuthn has a template tag that -can help with this by calling the right browser APIs to update the stored -Passkeys for you. +To keep passkeys up to date with user details, Django OTP WebAuthn provides the ``render_otp_webauthn_sync_signals_scripts`` template tag. It calls the appropriate browser APIs to update stored passkeys. -.. _the_render_otp_webauthn_sync_signals_scripts_template_tag: +.. _what_is_render_otp_webauthn_sync_signals_scripts: -The ``render_otp_webauthn_sync_signals_scripts`` template tag -------------------------------------------------------------- +What is ``render_otp_webauthn_sync_signals_scripts``? +----------------------------------------------------- -This is a lightweight template tag that you can add to your base template. It -looks for a key in the session that indicates that user details have changed, -and if so, it renders the necessary JavaScript to update the stored Passkeys in -the user's browser. If no syncing is needed, it outputs nothing so it won't -unnecessarily bloat your pages. +``render_otp_webauthn_sync_signals_scripts`` is a lightweight template tag that you can add to your base template to check for a key in the session. The key indicates that user details have changed. If the template tag detects the key, it renders the JavaScript needed to update the stored passkeys in the user's browser. -To use this template tag, you would add it near the bottom of your base -template, just before the closing ```` tag. This way it won't block the -initial page load, but will still be executed when the page is fully loaded. +To use the ``render_otp_webauthn_sync_signals_scripts`` template tag, add it near the bottom of your base template, just before the closing ```` tag. This ensures it doesn't block the initial page load, but still executes once the page is fully loaded. .. code-block:: html @@ -43,39 +31,21 @@ initial page load, but will still be executed when the page is fully loaded. -How it works ------------- - -When a user authenticates using a Passkey or registers a new Passkey, the -default authentication and registration views will automatically call -``django_otp_webauthn.utils.request_user_details_sync`` after successful -registration or authentication respectively. This function sets a flag in the -user's session indicating that their details need to be synchronized. This is -the official way to request a user details sync from Django OTP WebAuthn. - -On the next page load, when templates are being rendered, the template tag sees -this flag and outputs JavaScript code that uses the appropriate WebAuthn API to -update the stored Passkeys with the latest user details. After the -synchronization is complete, the flag is cleared from the session to prevent -rendering unnecessary the JavaScript on subsequent page loads. - -Removing deleted Passkeys from the browser ------------------------------------------- - -If a user removes a Passkey from your application – most likely through an -account settings page that you have created – this removal won't automatically -be reflected in the Passkeys stored in their browser. Meaning the Passkey will -still show up the next time the user tries to authenticate. - -To deal with this appropriately, you should call the -``request_user_details_sync`` utility function after a Passkey is removed. This -will ensure that the next time the user loads a page, their browser will be -informed about the deleted Passkey and can remove it from storage. Sometimes -this is associated with a prompt to the user to confirm the removal, depending -on the browser's implementation. - -In the event that the user does try to use a removed Passkey during -authentication, the browser will automatically be informed that the Passkey is +How ``render_otp_webauthn_sync_signals_scripts`` works +------------------------------------------------------ + +When a user authenticates using a passkey or registers a new one, the default authentication and registration views call the ``django_otp_webauthn.utils.request_user_details_sync`` function. This function sets a flag in the user's session, indicating that their details require synchronization. + +On the next page load, when the templates are rendered, ``render_otp_webauthn_sync_signals_scripts`` checks this flag and outputs JavaScript that uses the appropriate WebAuthn API to update the stored passkeys with the latest user details. After the synchronization is complete, the flag is cleared from the session to prevent rendering the JavaScript on subsequent page loads. + +Remove deleted Passkeys from the browser +---------------------------------------- + +When a user removes a passkey from your application, the browser doesn't automatically update its stored passkeys. So, the deleted passkey may still appear the next time the user tries to authenticate. + +To handle, call the ``request_user_details_sync`` utility function after you remove a passkey. This ensures that on the next page load, the browser receives the information it needs to remove the deleted passkey from storage. Depending on the browser’s implementation, the user may be prompted to confirm the removal. + +If the user tries to use a removed passkey during authentication, the browser automatically determines that the passkey is no longer valid through the `PublicKeyCredential.signalUnknownCredential `_ WebAuthn API. @@ -84,12 +54,7 @@ WebAuthn API. How to trigger user details sync -------------------------------- -To trigger the user details synchronization process, you need to call the -``request_user_details_sync`` utility function whenever a user's details are -updated. For example, if you have a view that allows users to update their email -address or username, you would add a call to this function after the update is -successful. This will ensure that the next time the user loads a page, their -Passkeys will be updated with the new details. +To trigger the user details synchronization process, call the ``request_user_details_sync`` utility function whenever a user updates their details. For example, if you have a view that lets users change their email address or username, add a call to this function after the update succeeds. This ensures that the next time the user loads a page, their passkeys are updated with the new details. .. code-block:: py @@ -110,12 +75,7 @@ Passkeys will be updated with the new details. form = UserDetailsForm(instance=request.user) return render(request, "update_user_details.html", {"form": form}) -By following these steps, you can ensure that your users' Passkeys remain -up to date with their latest details. - -You won't see any visible messages or indicators when the synchronization occurs, as -it happens silently in the background. However, you can check your browser's -console for any errors or logs related to the synchronization process if needed. +The synchronization happens in the background, so you won't see any messages or indicators when it occurs. If needed, you can check your browser’s console for any errors or logs related to the synchronization process. How does this work from a technical perspective? ------------------------------------------------ From dcc12cf4aaef2951cee61fa7cfd9cf6e1f7efe8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:07:13 +0000 Subject: [PATCH 20/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/how_to_guides/keeping_passkeys_in_sync.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to_guides/keeping_passkeys_in_sync.rst b/docs/how_to_guides/keeping_passkeys_in_sync.rst index 01b888e..fa76d1d 100644 --- a/docs/how_to_guides/keeping_passkeys_in_sync.rst +++ b/docs/how_to_guides/keeping_passkeys_in_sync.rst @@ -45,7 +45,7 @@ When a user removes a passkey from your application, the browser doesn't automat To handle, call the ``request_user_details_sync`` utility function after you remove a passkey. This ensures that on the next page load, the browser receives the information it needs to remove the deleted passkey from storage. Depending on the browser’s implementation, the user may be prompted to confirm the removal. -If the user tries to use a removed passkey during authentication, the browser automatically determines that the passkey is +If the user tries to use a removed passkey during authentication, the browser automatically determines that the passkey is no longer valid through the `PublicKeyCredential.signalUnknownCredential `_ WebAuthn API. From 7174f1b99b65b2d3de911cb023109e4b7ec167b6 Mon Sep 17 00:00:00 2001 From: Damilola Oladele <98895460+activus-d@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:42:16 +0100 Subject: [PATCH 21/21] Update docs/how_to_guides/keeping_passkeys_in_sync.rst --- docs/how_to_guides/keeping_passkeys_in_sync.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how_to_guides/keeping_passkeys_in_sync.rst b/docs/how_to_guides/keeping_passkeys_in_sync.rst index fa76d1d..aa892ba 100644 --- a/docs/how_to_guides/keeping_passkeys_in_sync.rst +++ b/docs/how_to_guides/keeping_passkeys_in_sync.rst @@ -51,8 +51,8 @@ no longer valid through the `PublicKeyCredential.signalUnknownCredential WebAuthn API. -How to trigger user details sync --------------------------------- +Trigger user details synchronization +------------------------------------ To trigger the user details synchronization process, call the ``request_user_details_sync`` utility function whenever a user updates their details. For example, if you have a view that lets users change their email address or username, add a call to this function after the update succeeds. This ensures that the next time the user loads a page, their passkeys are updated with the new details.