Compare commits

..

1 Commits

Author SHA1 Message Date
Dominic R
18d4f85579 sources/oauth: add AT Protocol source
Closes https://github.com/goauthentik/authentik/issues/22031
2026-05-04 19:46:25 -04:00
335 changed files with 2861 additions and 5789 deletions

View File

@@ -49,7 +49,7 @@ runs:
if: ${{ contains(inputs.dependencies, 'python') }}
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync --all-extras --dev --locked
run: uv sync --all-extras --dev --frozen
- name: Setup rust (stable)
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies, 'rust-nightly') }}
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
@@ -64,7 +64,7 @@ runs:
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2
uses: taiki-e/install-action@51cd0b8c0499559d9a4d75c0f5c67bec3a894ec8 # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (web)

117
Cargo.lock generated
View File

@@ -1003,17 +1003,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "evmap"
version = "11.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
dependencies = [
"hashbag",
"left-right",
"smallvec",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -1230,21 +1219,6 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1326,12 +1300,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "hashbag"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -1889,17 +1857,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "left-right"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a"
dependencies = [
"crossbeam-utils",
"loom",
"slab",
]
[[package]]
name = "libc"
version = "0.2.183"
@@ -1971,19 +1928,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -2033,12 +1977,11 @@ dependencies = [
[[package]]
name = "metrics-exporter-prometheus"
version = "0.18.3"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda"
dependencies = [
"base64 0.22.1",
"evmap",
"indexmap",
"metrics",
"metrics-util",
@@ -2057,7 +2000,7 @@ dependencies = [
"hashbrown 0.16.1",
"metrics",
"quanta",
"rand 0.9.4",
"rand 0.9.2",
"rand_xoshiro",
"sketches-ddsketch",
]
@@ -2744,7 +2687,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.4",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
@@ -2804,9 +2747,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
@@ -3160,12 +3103,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -3203,9 +3140,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "sentry"
version = "0.48.0"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8ac94aab850a23d7507307cc505332ed2bafd36c65930dfc5c43610f9e9b477"
checksum = "eb25f439f97d26fea01d717fa626167ceffcd981addaa670001e70505b72acbb"
dependencies = [
"cfg_aliases",
"httpdate",
@@ -3224,9 +3161,9 @@ dependencies = [
[[package]]
name = "sentry-backtrace"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84c325ace9ca2388e510fe7d6672b5d60cd8b3bd0eb4bb4ee8314c323cd686"
checksum = "46a8c2c1bd5c1f735e84f28b48e7d72efcaafc362b7541bc8253e60e8fcdffc6"
dependencies = [
"backtrace",
"regex",
@@ -3235,9 +3172,9 @@ dependencies = [
[[package]]
name = "sentry-contexts"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "896c1ab62dbfe1746fb262bbf72e6feb2fb9dfb2c14709077bf71beb532e44b2"
checksum = "9b88a90baa654d7f0e1f4b667f6b434293d9f72c71bef16b197c76af5b7d5803"
dependencies = [
"hostname",
"libc",
@@ -3249,11 +3186,11 @@ dependencies = [
[[package]]
name = "sentry-core"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f5abf20c42cb1593ec1638976e2647da55f79bccac956444c1707b6cce259a"
checksum = "0ac170a5bba8bec6e3339c90432569d89641fa7a3d3e4f44987d24f0762e6adf"
dependencies = [
"rand 0.9.4",
"rand 0.9.2",
"sentry-types",
"serde",
"serde_json",
@@ -3262,9 +3199,9 @@ dependencies = [
[[package]]
name = "sentry-debug-images"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b88bbe6a760d5724bb40689827e82e8db1e275947df2c59abe171bfc30bb671"
checksum = "dd9646a972b57896d4a92ed200cf76139f8e30b3cfd03b6662ae59926d26633c"
dependencies = [
"findshlibs",
"sentry-core",
@@ -3272,9 +3209,9 @@ dependencies = [
[[package]]
name = "sentry-panic"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0260dcb52562b6a79ae7702312a26dba94b79fb5baee7301087529e5ca4e872e"
checksum = "6127d3d304ba5ce0409401e85aae538e303a569f8dbb031bf64f9ba0f7174346"
dependencies = [
"sentry-backtrace",
"sentry-core",
@@ -3282,9 +3219,9 @@ dependencies = [
[[package]]
name = "sentry-tower"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d669616d5d5279b5712febfc80c343acc3695e499de0d101ed70fceacadf37f2"
checksum = "61c5253dc4ad89863a866b93aeaaac1c9d60f2f774663b5024afe2d57e0a101c"
dependencies = [
"sentry-core",
"tower-layer",
@@ -3293,9 +3230,9 @@ dependencies = [
[[package]]
name = "sentry-tracing"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1c035f3a0a8671ae1a231c5b457abb68b71acba2bf3054dab2a09a9d4ea487e"
checksum = "27701acc51e68db5281802b709010395bfcbcb128b1d0a4e5873680d3b47ff0c"
dependencies = [
"bitflags 2.11.0",
"sentry-backtrace",
@@ -3306,13 +3243,13 @@ dependencies = [
[[package]]
name = "sentry-types"
version = "0.48.1"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d8e81058ec155992191f61c7b29bfa7b2cf12012131e7cdc0678020898a7c9"
checksum = "56780cb5597d676bf22e6c11d1f062eb4def46390ea3bfb047bcbcf7dfd19bdb"
dependencies = [
"debugid",
"hex",
"rand 0.9.4",
"rand 0.9.2",
"serde",
"serde_json",
"thiserror 2.0.18",
@@ -4214,7 +4151,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand 0.9.4",
"rand 0.9.2",
"sha1",
"thiserror 2.0.18",
]

View File

@@ -44,7 +44,7 @@ hyper-util = "= 0.1.20"
ipnet = { version = "= 2.12.0", features = ["serde"] }
json-subscriber = "= 0.2.8"
metrics = "= 0.24.5"
metrics-exporter-prometheus = { version = "= 0.18.3", default-features = false }
metrics-exporter-prometheus = { version = "= 0.18.1", default-features = false }
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
notify = "= 8.2.0"
pin-project-lite = "= 0.2.17"
@@ -67,7 +67,7 @@ reqwest-middleware = { version = "= 0.5.1", features = [
"rustls",
] }
rustls = { version = "= 0.23.40", features = ["fips"] }
sentry = { version = "= 0.48.0", default-features = false, features = [
sentry = { version = "= 0.47.0", default-features = false, features = [
"backtrace",
"contexts",
"debug-images",

View File

@@ -1,6 +1,5 @@
"""Serializer mixin for managed models"""
from json import JSONDecodeError, loads
from typing import cast
from django.conf import settings
@@ -45,7 +44,6 @@ class BlueprintUploadSerializer(PassiveSerializer):
file = FileField(required=False)
path = CharField(required=False)
context = CharField(required=False, allow_blank=True)
def validate_path(self, path: str) -> str:
"""Ensure the path (if set) specified is retrievable"""
@@ -56,18 +54,6 @@ class BlueprintUploadSerializer(PassiveSerializer):
raise ValidationError(_("Blueprint file does not exist"))
return path
def validate_context(self, context: str) -> dict:
"""Parse context as a JSON object"""
if not context:
return {}
try:
parsed = loads(context)
except JSONDecodeError as exc:
raise ValidationError(_("Context must be valid JSON")) from exc
if not isinstance(parsed, dict):
raise ValidationError(_("Context must be a JSON object"))
return parsed
class ManagedSerializer:
"""Managed Serializer"""
@@ -217,7 +203,10 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
request={"multipart/form-data": BlueprintUploadSerializer},
responses={200: BlueprintImportResultSerializer},
responses={
204: BlueprintImportResultSerializer,
400: BlueprintImportResultSerializer,
},
)
@action(url_path="import", detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
@validate(
@@ -235,8 +224,7 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
).retrieve_file()
else:
raise ValidationError("Either path or file must be set")
context = body.validated_data.get("context") or {}
importer = Importer.from_string(string_contents, context)
importer = Importer.from_string(string_contents)
check_blueprint_perms(importer.blueprint, request.user)
@@ -244,13 +232,21 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
import_response = self.BlueprintImportResultSerializer(
data={
"logs": [LogEventSerializer(log).data for log in logs],
"success": valid,
"logs": [],
"success": False,
}
)
import_response.is_valid(raise_exception=True)
if valid:
import_response.initial_data["success"] = importer.apply()
import_response.is_valid()
import_response.initial_data["logs"] = [LogEventSerializer(log).data for log in logs]
import_response.initial_data["success"] = valid
import_response.is_valid()
if not valid:
return Response(data=import_response.initial_data, status=200)
successful = importer.apply()
import_response.initial_data["success"] = successful
import_response.is_valid()
if not successful:
return Response(data=import_response.initial_data, status=200)
return Response(data=import_response.initial_data, status=200)

View File

@@ -1,19 +1,14 @@
"""Test blueprints v1 api"""
from json import dumps, loads
from json import loads
from tempfile import NamedTemporaryFile, mkdtemp
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from rest_framework.test import APITestCase
from yaml import dump
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.invitation.models import InvitationStage
from authentik.stages.user_write.models import UserWriteStage
TMP = mkdtemp("authentik-blueprints")
@@ -85,121 +80,3 @@ class TestBlueprintsV1API(APITestCase):
res.content.decode(),
{"content": ["Failed to validate blueprint", "- Invalid blueprint version"]},
)
def test_api_import_with_context(self):
"""Test that the import endpoint applies the supplied context to the real blueprint"""
slug = f"invitation-enrollment-{generate_id()}"
flow_name = f"Invitation Enrollment {generate_id()}"
stage_name = f"invitation-stage-{generate_id()}"
user_type = "internal"
continue_without_invitation = True
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": dumps(
{
"flow_slug": slug,
"flow_name": flow_name,
"stage_name": stage_name,
"continue_flow_without_invitation": continue_without_invitation,
"user_type": user_type,
}
),
},
format="multipart",
)
self.assertEqual(res.status_code, 200)
self.assertTrue(res.json()["success"])
flow = Flow.objects.get(slug=slug)
self.assertEqual(flow.name, flow_name)
self.assertEqual(flow.title, flow_name)
invitation_stage = InvitationStage.objects.get(name=stage_name)
self.assertEqual(
invitation_stage.continue_flow_without_invitation,
continue_without_invitation,
)
user_write_stage = UserWriteStage.objects.get(
name=f"invitation-enrollment-user-write-{slug}"
)
self.assertEqual(user_write_stage.user_type, user_type)
self.assertEqual(user_write_stage.user_path_template, f"users/{user_type}")
def test_api_import_blank_path(self):
"""Validator returns empty path unchanged (covers api.py:53)."""
with NamedTemporaryFile(mode="w+", suffix=".yaml") as file:
file.write(dump({"version": 1, "entries": []}))
file.flush()
file.seek(0)
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={"path": "", "file": file},
format="multipart",
)
self.assertEqual(res.status_code, 200)
def test_api_import_invalid_blueprint_returns_result_payload(self):
"""Invalid blueprint content returns a result payload instead of a 400 response."""
file = SimpleUploadedFile("invalid-blueprint.yaml", b'{"version": 3}')
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={"file": file},
format="multipart",
)
self.assertEqual(res.status_code, 200)
self.assertFalse(res.json()["success"])
self.assertGreater(len(res.json()["logs"]), 0)
def test_api_import_unknown_path(self):
"""Path not in available blueprints is rejected (covers api.py:56)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={"path": "does/not/exist.yaml"},
format="multipart",
)
self.assertEqual(res.status_code, 400)
self.assertIn("Blueprint file does not exist", res.content.decode())
def test_api_import_blank_context(self):
"""Blank context is normalized to empty dict (covers api.py:62)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": "",
},
format="multipart",
)
self.assertEqual(res.status_code, 200)
def test_api_import_invalid_json_context(self):
"""Malformed JSON context raises ValidationError (covers api.py:65-66)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": "{not json",
},
format="multipart",
)
self.assertEqual(res.status_code, 400)
self.assertIn("Context must be valid JSON", res.content.decode())
def test_api_import_non_object_context(self):
"""JSON context that isn't an object is rejected (covers api.py:68)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": "[1, 2, 3]",
},
format="multipart",
)
self.assertEqual(res.status_code, 400)
self.assertIn("Context must be a JSON object", res.content.decode())

View File

@@ -32,19 +32,19 @@ from authentik.rbac.decorators import permission_required
class UserAgentDeviceDict(TypedDict):
"""User agent device"""
brand: str | None = None
brand: str
family: str
model: str | None = None
model: str
class UserAgentOSDict(TypedDict):
"""User agent os"""
family: str
major: str | None = None
minor: str | None = None
patch: str | None = None
patch_minor: str | None = None
major: str
minor: str
patch: str
patch_minor: str
class UserAgentBrowserDict(TypedDict):

View File

@@ -29,7 +29,6 @@ class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others
default = False
visibility = "public"
description = _("Refresh other tabs after successful authentication.")
deprecated = True
class ContinuousLogin(Flag[bool], key="flows_continuous_login"):

View File

@@ -187,7 +187,6 @@ SPECTACULAR_SETTINGS = {
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
"RedirectURITypeEnum": "authentik.providers.oauth2.models.RedirectURIType",
"SAMLBindingsEnum": "authentik.providers.saml.models.SAMLBindings",
"SAMLLogoutMethods": "authentik.providers.saml.models.SAMLLogoutMethods",
"SAMLNameIDPolicyEnum": "authentik.sources.saml.models.SAMLNameIDPolicy",

View File

@@ -33,6 +33,7 @@ class SourceTypeSerializer(PassiveSerializer):
profile_url = CharField(read_only=True, allow_null=True)
oidc_well_known_url = CharField(read_only=True, allow_null=True)
oidc_jwks_url = CharField(read_only=True, allow_null=True)
client_secret_required = BooleanField()
class OAuthSourceSerializer(SourceSerializer):
@@ -65,6 +66,15 @@ class OAuthSourceSerializer(SourceSerializer):
)
source_type = registry.find_type(provider_type_name)
if not source_type.client_secret_required and "consumer_secret" not in attrs:
attrs["consumer_secret"] = ""
if (
source_type.client_secret_required
and not self.instance
and not attrs.get("consumer_secret")
):
raise ValidationError({"consumer_secret": "This field is required."})
well_known = attrs.get("oidc_well_known_url") or source_type.oidc_well_known_url
inferred_oidc_jwks_url = None
@@ -149,7 +159,7 @@ class OAuthSourceSerializer(SourceSerializer):
"authorization_code_auth_method",
]
extra_kwargs = {
"consumer_secret": {"write_only": True},
"consumer_secret": {"write_only": True, "allow_blank": True, "required": False},
"request_token_url": {"allow_blank": True},
"authorization_url": {"allow_blank": True},
"access_token_url": {"allow_blank": True},

View File

@@ -10,6 +10,7 @@ LOGGER = get_logger()
AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.apple",
"authentik.sources.oauth.types.atproto",
"authentik.sources.oauth.types.azure_ad",
"authentik.sources.oauth.types.discord",
"authentik.sources.oauth.types.entra_id",

View File

@@ -271,6 +271,15 @@ class EntraIDOAuthSource(CreatableType, OAuthSource):
verbose_name_plural = _("Entra ID OAuth Sources")
class AtProtoOAuthSource(CreatableType, OAuthSource):
"""Social Login using AT Protocol."""
class Meta:
abstract = True
verbose_name = _("AT Protocol OAuth Source")
verbose_name_plural = _("AT Protocol OAuth Sources")
class OpenIDConnectOAuthSource(CreatableType, OAuthSource):
"""Login using a Generic OpenID-Connect compliant provider."""

View File

@@ -0,0 +1,284 @@
"""AT Protocol OAuth Source tests"""
from urllib.parse import parse_qs, urlparse
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat
from django.test import RequestFactory, SimpleTestCase
from jwt import decode, get_unverified_header
from requests_mock import Mocker
from authentik.sources.oauth.api.source import OAuthSourceSerializer
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.atproto import (
BSKY_AUTHORIZATION_URL_DEFAULT,
BSKY_PAR_URL_DEFAULT,
BSKY_PUBLIC_PROFILE_URL_DEFAULT,
BSKY_TOKEN_URL_DEFAULT,
AtProtoOAuthClient,
AtProtoType,
)
ATPROTO_DID = "did:plc:z72i7hdynmk6r22z27h6tvur"
ATPROTO_PDS = "https://puffball.us-east.host.bsky.network"
ATPROTO_CLIENT_ID = "https://authentik.example/application/o/atproto/client-metadata.json"
ATPROTO_DID_DOCUMENT = {
"id": ATPROTO_DID,
"alsoKnownAs": ["at://bsky.app"],
"service": [
{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": ATPROTO_PDS,
}
],
}
ATPROTO_PROFILE = {
"did": ATPROTO_DID,
"handle": "bsky.app",
"displayName": "Bluesky",
}
CUSTOM_ISSUER = "https://auth.example"
CUSTOM_AUTHORIZATION_URL = f"{CUSTOM_ISSUER}/oauth/authorize"
CUSTOM_PAR_URL = f"{CUSTOM_ISSUER}/oauth/par"
CUSTOM_TOKEN_URL = f"{CUSTOM_ISSUER}/oauth/token"
CUSTOM_PROFILE_URL = f"{CUSTOM_ISSUER}/xrpc/app.bsky.actor.getProfile"
def private_key_pem() -> str:
"""Generate an ES256 private key for DPoP tests."""
return (
ec.generate_private_key(ec.SECP256R1())
.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
.decode()
)
class TestTypeAtProto(SimpleTestCase):
"""AT Protocol OAuth Source tests"""
def setUp(self):
self.source = OAuthSource(
name="test",
slug="test",
provider_type="atproto",
consumer_key=ATPROTO_CLIENT_ID,
)
self.factory = RequestFactory()
def get_request(self):
request = self.factory.get("/")
request.session = {}
return request
def get_callback_request(self, issuer: str = "https://bsky.social"):
request = self.factory.get(f"/?state=state&iss={issuer}&code=code")
request.session = {
"authentik/sources/oauth/atproto/test": {
"state": "state",
"code_verifier": "verifier",
"issuer": issuer,
"private_key": private_key_pem(),
"dpop_nonce": "nonce-1",
"login_hint": None,
"expected_did": None,
}
}
return request
def test_enroll_context(self):
"""Test AT Protocol enrollment context."""
ak_context = AtProtoType().get_base_user_properties(
source=self.source,
info=ATPROTO_PROFILE,
)
self.assertEqual(ak_context["username"], ATPROTO_PROFILE["handle"])
self.assertEqual(ak_context["name"], ATPROTO_PROFILE["displayName"])
self.assertIsNone(ak_context["email"])
def test_serializer_allows_missing_secret(self):
"""Test AT Protocol sources can be created without a client secret."""
serializer = OAuthSourceSerializer()
validated = serializer.validate(
{
"name": "test-atproto",
"slug": "test-atproto",
"provider_type": "atproto",
"consumer_key": ATPROTO_CLIENT_ID,
}
)
self.assertEqual(validated["consumer_secret"], "")
@Mocker()
def test_redirect_uses_par_dpop_pkce_and_no_secret(self, mock: Mocker):
"""Test authorization starts with a DPoP-bound pushed authorization request."""
mock.post(
BSKY_PAR_URL_DEFAULT,
json={"request_uri": "urn:request:123"},
headers={"DPoP-Nonce": "nonce-1"},
)
request = self.get_request()
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
redirect_url = client.get_redirect_url({"scope": ["atproto", "transition:generic"]})
parsed_redirect = urlparse(redirect_url)
parsed_query = parse_qs(parsed_redirect.query)
parsed_redirect_url = (
f"{parsed_redirect.scheme}://{parsed_redirect.netloc}{parsed_redirect.path}"
)
self.assertEqual(parsed_redirect_url, BSKY_AUTHORIZATION_URL_DEFAULT)
self.assertEqual(parsed_query["client_id"], [ATPROTO_CLIENT_ID])
self.assertEqual(parsed_query["request_uri"], ["urn:request:123"])
self.assertEqual(len(mock.request_history), 1)
par_request = mock.request_history[0]
self.assertIn("DPoP", par_request.headers)
self.assertEqual(par_request.text.count("client_secret"), 0)
self.assertIn("client_id=https%3A%2F%2Fauthentik.example", par_request.text)
self.assertIn("code_challenge_method=S256", par_request.text)
self.assertIn("scope=atproto+transition%3Ageneric", par_request.text)
header = get_unverified_header(par_request.headers["DPoP"])
payload = decode(par_request.headers["DPoP"], options={"verify_signature": False})
self.assertEqual(header["typ"], "dpop+jwt")
self.assertEqual(header["alg"], "ES256")
self.assertEqual(payload["htm"], "POST")
self.assertEqual(payload["htu"], BSKY_PAR_URL_DEFAULT)
@Mocker()
def test_custom_urls_override_bluesky_defaults(self, mock: Mocker):
"""Test non-Bluesky AT Protocol endpoint configuration."""
source = OAuthSource(
name="test",
slug="test",
provider_type="atproto",
consumer_key=ATPROTO_CLIENT_ID,
authorization_url=CUSTOM_AUTHORIZATION_URL,
request_token_url=CUSTOM_PAR_URL,
access_token_url=CUSTOM_TOKEN_URL,
profile_url=CUSTOM_PROFILE_URL,
)
mock.post(
CUSTOM_PAR_URL,
json={"request_uri": "urn:request:custom"},
headers={"DPoP-Nonce": "nonce-custom"},
)
request = self.get_request()
client = AtProtoOAuthClient(source, request, callback="/callback/")
redirect_url = client.get_redirect_url({"scope": ["atproto"]})
parsed_redirect = urlparse(redirect_url)
self.assertEqual(
f"{parsed_redirect.scheme}://{parsed_redirect.netloc}{parsed_redirect.path}",
CUSTOM_AUTHORIZATION_URL,
)
self.assertEqual(request.session[client.session_key]["issuer"], CUSTOM_ISSUER)
self.assertEqual(mock.request_history[0].url, CUSTOM_PAR_URL)
@Mocker()
def test_access_token_validates_subject_scope_and_issuer(self, mock: Mocker):
"""Test callback token response validation."""
mock.post(
BSKY_TOKEN_URL_DEFAULT,
json={
"access_token": "access",
"refresh_token": "refresh",
"token_type": "DPoP",
"expires_in": 300,
"sub": ATPROTO_DID,
"scope": "atproto transition:generic",
},
headers={"DPoP-Nonce": "nonce-2"},
)
mock.get(f"https://plc.directory/{ATPROTO_DID}", json=ATPROTO_DID_DOCUMENT)
mock.get(
f"{ATPROTO_PDS}/.well-known/oauth-protected-resource",
json={"authorization_servers": ["https://bsky.social"]},
)
request = self.get_callback_request()
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
token = client.get_access_token()
self.assertEqual(token["sub"], ATPROTO_DID)
self.assertEqual(token["pds_url"], ATPROTO_PDS)
token_request = mock.request_history[0]
self.assertIn("DPoP", token_request.headers)
self.assertEqual(token_request.text.count("client_secret"), 0)
self.assertIn("code_verifier=verifier", token_request.text)
@Mocker()
def test_access_token_rejects_non_dpop_token_type(self, mock: Mocker):
"""Test callback rejects token responses that are not DPoP-bound."""
mock.post(
BSKY_TOKEN_URL_DEFAULT,
json={
"access_token": "access",
"token_type": "Bearer",
"sub": ATPROTO_DID,
"scope": "atproto",
},
headers={"DPoP-Nonce": "nonce-2"},
)
client = AtProtoOAuthClient(self.source, self.get_callback_request(), callback="/callback/")
token = client.get_access_token()
self.assertEqual(token["error"], "Token response did not include a DPoP token type.")
@Mocker()
def test_did_web_localhost_uses_http_for_local_testing(self, mock: Mocker):
"""Test did:web localhost resolution for the local AT Protocol simulator."""
mock.get("http://localhost:8787/.well-known/did.json", json={"id": "did:web:localhost"})
client = AtProtoOAuthClient(self.source, self.get_request(), callback="/callback/")
document = client.get_did_document("did:web:localhost%3A8787")
self.assertEqual(document["id"], "did:web:localhost")
@Mocker()
def test_profile_info(self, mock: Mocker):
"""Test public Bluesky profile lookup."""
mock.get(BSKY_PUBLIC_PROFILE_URL_DEFAULT, json=ATPROTO_PROFILE)
client = AtProtoOAuthClient(self.source, self.get_request(), callback="/callback/")
profile = client.get_profile_info({"sub": ATPROTO_DID})
self.assertEqual(profile["did"], ATPROTO_DID)
self.assertEqual(profile["handle"], "bsky.app")
@Mocker()
def test_profile_info_with_transition_email(self, mock: Mocker):
"""Test private session email lookup when transition:email is granted."""
mock.get(BSKY_PUBLIC_PROFILE_URL_DEFAULT, json=ATPROTO_PROFILE)
mock.get(
f"{ATPROTO_PDS}/xrpc/com.atproto.server.getSession",
json={"email": "user@example.com", "emailConfirmed": True},
headers={"DPoP-Nonce": "nonce-3"},
)
request = self.get_request()
request.session = {
"authentik/sources/oauth/atproto/test": {
"state": "state",
"code_verifier": "verifier",
"issuer": "https://bsky.social",
"private_key": private_key_pem(),
"dpop_nonce": "nonce-2",
"login_hint": None,
"expected_did": None,
}
}
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
profile = client.get_profile_info(
{
"sub": ATPROTO_DID,
"scope": "atproto transition:email",
"access_token": "access",
"pds_url": ATPROTO_PDS,
}
)
self.assertEqual(profile["email"], "user@example.com")
session_request = mock.request_history[1]
self.assertEqual(session_request.headers["Authorization"], "DPoP access")
payload = decode(session_request.headers["DPoP"], options={"verify_signature": False})
self.assertIn("ath", payload)

View File

@@ -0,0 +1,486 @@
"""AT Protocol OAuth Views"""
from time import time
from typing import Any
from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse, urlunparse
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.hashes import SHA256, Hash
from cryptography.hazmat.primitives.serialization import (
Encoding,
NoEncryption,
PrivateFormat,
load_pem_private_key,
)
from django.templatetags.static import static
from django.urls import reverse
from django.utils.crypto import constant_time_compare, get_random_string
from jwt import encode
from jwt.algorithms import ECAlgorithm
from jwt.utils import base64url_encode
from requests.exceptions import RequestException
from structlog.stdlib import get_logger
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.utils import pkce_s256_challenge
from authentik.sources.oauth.clients.base import BaseOAuthClient
from authentik.sources.oauth.models import OAuthSource, PKCEMethod
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
# Bluesky defaults. AT Protocol OAuth requires these endpoint roles, but
# non-Bluesky deployments can use different hosts through the source URL fields.
BSKY_AUTHORIZATION_URL_DEFAULT = "https://bsky.social/oauth/authorize"
BSKY_TOKEN_URL_DEFAULT = "https://bsky.social/oauth/token" # nosec
BSKY_PAR_URL_DEFAULT = "https://bsky.social/oauth/par"
BSKY_ISSUER_DEFAULT = "https://bsky.social"
BSKY_PUBLIC_PROFILE_URL_DEFAULT = "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile"
HTTP_STATUS_BAD_REQUEST = 400
SESSION_KEY_ATPROTO = "authentik/sources/oauth/atproto"
class AtProtoOAuthClient(BaseOAuthClient):
"""AT Protocol OAuth client.
AT Protocol looks like OAuth2 from a distance, but the required security
profile is different enough that sharing the generic OAuth2 client would
hide important behavior: PAR is mandatory, access tokens are DPoP-bound,
public clients use metadata URLs instead of secrets, and the token subject
is the user's DID rather than an OIDC userinfo subject.
"""
def get_client_id(self) -> str:
"""Return the public client metadata URL."""
return self.source.consumer_key
@property
def session_key(self) -> str:
return f"{SESSION_KEY_ATPROTO}/{self.source.slug}"
def get_authorization_url(self) -> str:
if self.source.source_type.urls_customizable and self.source.authorization_url:
return self.source.authorization_url
return self.source.source_type.authorization_url or BSKY_AUTHORIZATION_URL_DEFAULT
def get_token_url(self) -> str:
if self.source.source_type.urls_customizable and self.source.access_token_url:
return self.source.access_token_url
return self.source.source_type.access_token_url or BSKY_TOKEN_URL_DEFAULT
def get_par_url(self) -> str:
if self.source.source_type.urls_customizable and self.source.request_token_url:
return self.source.request_token_url
return self.source.source_type.request_token_url or BSKY_PAR_URL_DEFAULT
def get_issuer(self) -> str:
parsed_url = urlparse(self.get_authorization_url())
if parsed_url.scheme and parsed_url.netloc:
return f"{parsed_url.scheme}://{parsed_url.netloc}"
return BSKY_ISSUER_DEFAULT
def get_redirect_args(self) -> dict[str, str]:
"""AT Protocol redirects are built from PAR responses instead."""
raise NotImplementedError
def get_redirect_url(self, parameters=None):
"""Create a PAR request and redirect with request_uri."""
request_uri = self.create_pushed_authorization_request(parameters or {})
parsed_url = urlparse(self.get_authorization_url())
parsed_args = parse_qs(parsed_url.query)
args = {
"client_id": self.get_client_id(),
"request_uri": request_uri,
}
args.update(parsed_args)
params = urlencode(args, quote_via=quote, doseq=True)
return urlunparse(parsed_url._replace(query=params))
def create_pushed_authorization_request(self, parameters: dict[str, Any]) -> str:
"""Create the pushed authorization request and persist session data."""
state = get_random_string(32)
code_verifier = generate_id(length=128)
private_key = ec.generate_private_key(ec.SECP256R1())
login_hint = parameters.pop("login_hint", None)
scope = parameters.pop("scope", [])
if isinstance(scope, str):
scopes = scope.split()
else:
scopes = list(scope)
if "atproto" not in scopes:
scopes.append("atproto")
# The DPoP key and PKCE verifier must survive the browser redirect so
# the callback can prove it is the same client that created the PAR.
session_data = {
"state": state,
"code_verifier": code_verifier,
"issuer": self.get_issuer(),
"private_key": private_key.private_bytes(
Encoding.PEM,
PrivateFormat.PKCS8,
NoEncryption(),
).decode(),
"dpop_nonce": None,
"login_hint": login_hint,
"expected_did": self.resolve_identifier(login_hint) if login_hint else None,
}
self.request.session[self.session_key] = session_data
# AT Protocol starts the browser flow with a PAR request. The browser
# only receives a request_uri, not the full authorization parameters.
body = {
"client_id": self.get_client_id(),
"response_type": "code",
"redirect_uri": self.request.build_absolute_uri(self.callback),
"scope": " ".join(sorted(set(scopes))),
"state": state,
"code_challenge": pkce_s256_challenge(code_verifier),
"code_challenge_method": PKCEMethod.S256,
}
if login_hint:
body["login_hint"] = login_hint
body.update(parameters)
response = self.request_with_dpop("post", self.get_par_url(), data=body)
try:
request_uri = response.json().get("request_uri")
except ValueError as exc:
raise RequestException("PAR response was not valid JSON", response=response) from exc
if not request_uri:
raise RequestException("PAR response did not include request_uri", response=response)
return request_uri
def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
"""Fetch the initial access token from the callback code."""
session_data = self.request.session.get(self.session_key)
if not session_data:
LOGGER.warning("No AT Protocol OAuth session found")
return {"error": "No AT Protocol OAuth session found."}
if not constant_time_compare(session_data["state"], self.get_request_arg("state", "")):
LOGGER.warning("AT Protocol OAuth state check failed")
return {"error": "State check failed."}
issuer = self.get_request_arg("iss")
if not issuer or not constant_time_compare(session_data["issuer"], issuer):
LOGGER.warning("AT Protocol OAuth issuer check failed", issuer=issuer)
return {"error": "Issuer check failed."}
code = self.get_request_arg("code")
if not code:
return {"error": self.get_request_arg("error_description") or "No token received."}
data = {
"grant_type": "authorization_code",
"client_id": self.get_client_id(),
"redirect_uri": self.request.build_absolute_uri(self.callback),
"code": code,
"code_verifier": session_data["code_verifier"],
}
try:
response = self.request_with_dpop("post", self.get_token_url(), data=data)
token = response.json()
except ValueError as exc:
LOGGER.warning("AT Protocol token response was not valid JSON", exc=exc)
return None
except RequestException as exc:
LOGGER.warning(
"Unable to fetch AT Protocol access token",
exc=exc,
response=exc.response.text if exc.response is not None else str(exc),
)
return None
validation_error = self.validate_token_response(token, session_data, issuer)
if validation_error:
return {"error": validation_error}
return token
def validate_token_response(
self,
token: dict[str, Any],
session_data: dict[str, Any],
issuer: str,
) -> str | None:
"""Validate AT Protocol token claims and attach the verified PDS URL."""
# The token response identifies the account by DID. That DID becomes
# the stable source connection identifier in authentik.
did = token.get("sub")
if not did:
return "Token response did not include an account DID."
if "atproto" not in token.get("scope", "").split():
return "Token response did not include the atproto scope."
if token.get("token_type") != "DPoP":
return "Token response did not include a DPoP token type."
expected_did = session_data.get("expected_did")
if expected_did and not constant_time_compare(expected_did, did):
LOGGER.warning("AT Protocol OAuth subject check failed", expected=expected_did, did=did)
return "Subject check failed."
# Verify the DID document's PDS points back to the authorization server
# that issued the callback, otherwise a token could claim another DID.
pds_url = self.get_pds_url_for_subject(did, issuer)
if not pds_url:
return "Issuer is not authoritative for this account."
token["pds_url"] = pds_url
return None
def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any] | None:
"""Fetch public profile data for the authenticated DID."""
did = token.get("sub")
if not did:
return None
profile_url = BSKY_PUBLIC_PROFILE_URL_DEFAULT
if self.source.source_type.urls_customizable and self.source.profile_url:
profile_url = self.source.profile_url
response = self.session.get(profile_url, params={"actor": did})
try:
response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to fetch AT Protocol profile",
exc=exc,
response=exc.response.text if exc.response is not None else str(exc),
)
return {"did": did}
profile = response.json()
profile["did"] = did
if "transition:email" in token.get("scope", "").split() and token.get("pds_url"):
profile.update(self.get_session_info(token))
return profile
def request_with_dpop(self, method: str, url: str, **kwargs):
"""Make a DPoP request, retrying once when the server provides a fresh nonce."""
response = self.do_dpop_request(method, url, **kwargs)
if response.status_code == HTTP_STATUS_BAD_REQUEST and response.headers.get("DPoP-Nonce"):
self.update_dpop_nonce(response.headers["DPoP-Nonce"])
response = self.do_dpop_request(method, url, **kwargs)
response.raise_for_status()
nonce = response.headers.get("DPoP-Nonce")
if not nonce:
raise RequestException("DPoP response did not include DPoP-Nonce", response=response)
self.update_dpop_nonce(nonce)
return response
def get_session_info(self, token: dict[str, Any]) -> dict[str, Any]:
"""Fetch private session data when transition:email was granted."""
pds_url = token["pds_url"].rstrip("/")
session_url = f"{pds_url}/xrpc/com.atproto.server.getSession"
headers = {
"Authorization": f"DPoP {token['access_token']}",
}
response = self.do_dpop_request(
"get",
session_url,
headers=headers,
access_token=token["access_token"],
)
if response.status_code == HTTP_STATUS_BAD_REQUEST and response.headers.get("DPoP-Nonce"):
self.update_dpop_nonce(response.headers["DPoP-Nonce"])
response = self.do_dpop_request(
"get",
session_url,
headers=headers,
access_token=token["access_token"],
)
try:
response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to fetch AT Protocol session info",
exc=exc,
response=exc.response.text if exc.response is not None else str(exc),
)
return {}
nonce = response.headers.get("DPoP-Nonce")
if nonce:
self.update_dpop_nonce(nonce)
try:
return response.json()
except ValueError as exc:
LOGGER.warning("AT Protocol session response was not valid JSON", exc=exc)
return {}
def do_dpop_request(self, method: str, url: str, **kwargs):
access_token = kwargs.pop("access_token", None)
headers = dict(kwargs.pop("headers", {}))
headers["Accept"] = "application/json"
headers["DPoP"] = self.build_dpop_proof(method, url, access_token)
return self.session.request(method, url, headers=headers, **kwargs)
def build_dpop_proof(self, method: str, url: str, access_token: str | None = None) -> str:
session_data = self.request.session[self.session_key]
private_key = load_pem_private_key(session_data["private_key"].encode(), password=None)
if not isinstance(private_key, EllipticCurvePrivateKey):
raise TypeError("DPoP private key must be an EC key")
payload = {
"jti": generate_id(),
"htm": method.upper(),
"htu": url,
"iat": int(time()),
}
if session_data.get("dpop_nonce"):
payload["nonce"] = session_data["dpop_nonce"]
if access_token:
# Resource requests bind the proof to the access token with ath.
digest = Hash(SHA256())
digest.update(access_token.encode())
payload["ath"] = base64url_encode(digest.finalize()).decode()
public_jwk = ECAlgorithm.to_jwk(private_key.public_key(), as_dict=True)
public_jwk.pop("kid", None)
return encode(
payload,
private_key,
algorithm="ES256",
headers={
"typ": "dpop+jwt",
"jwk": public_jwk,
},
)
def update_dpop_nonce(self, nonce: str) -> None:
session_data = self.request.session[self.session_key]
session_data["dpop_nonce"] = nonce
self.request.session[self.session_key] = session_data
def get_request_arg(self, key: str, default: Any | None = None) -> Any:
if self.request.method == "POST":
return self.request.POST.get(key, default)
return self.request.GET.get(key, default)
def resolve_identifier(self, identifier: str | None) -> str | None:
"""Resolve a handle or DID to a DID."""
if not identifier:
return None
if identifier.startswith("did:"):
return identifier
response = self.session.get(
f"{self.get_issuer()}/xrpc/com.atproto.identity.resolveHandle",
params={"handle": identifier.removeprefix("@")},
)
try:
response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to resolve AT Protocol login hint",
identifier=identifier,
exc=exc,
)
return None
try:
return response.json().get("did")
except ValueError as exc:
LOGGER.warning("AT Protocol handle resolution response was not valid JSON", exc=exc)
return None
def get_pds_url_for_subject(self, did: str, issuer: str) -> str | None:
"""Verify that the DID's PDS resolves to the callback issuer."""
try:
did_document = self.get_did_document(did)
pds_url = self.get_pds_url(did_document)
if not pds_url:
LOGGER.warning("DID document does not include an atproto PDS", did=did)
return None
resource_metadata = self.session.get(
f"{pds_url.rstrip('/')}/.well-known/oauth-protected-resource"
)
resource_metadata.raise_for_status()
try:
authorization_servers = resource_metadata.json().get("authorization_servers", [])
except ValueError as exc:
raise RequestException(
"OAuth protected resource metadata was not valid JSON",
response=resource_metadata,
) from exc
except RequestException as exc:
LOGGER.warning("Unable to verify AT Protocol issuer", did=did, issuer=issuer, exc=exc)
return None
if issuer in authorization_servers:
return pds_url
return None
def get_did_document(self, did: str) -> dict[str, Any]:
if did.startswith("did:plc:"):
response = self.session.get(f"https://plc.directory/{did}")
elif did.startswith("did:web:"):
# did:web resolves by fetching a DID document from the hostname in the DID.
# The AT Protocol local simulator uses did:web:localhost, which cannot use
# HTTPS locally; real did:web identities should resolve over HTTPS.
did_parts = [unquote(part) for part in did.removeprefix("did:web:").split(":")]
host = did_parts[0]
path = "/".join(did_parts[1:])
scheme = "http" if host.startswith(("localhost", "127.0.0.1")) else "https"
did_path = f"{path}/did.json" if path else ".well-known/did.json"
response = self.session.get(f"{scheme}://{host}/{did_path}")
else:
raise RequestException(f"Unsupported DID method: {did}")
response.raise_for_status()
try:
return response.json()
except ValueError as exc:
raise RequestException("DID document was not valid JSON", response=response) from exc
def get_pds_url(self, did_document: dict[str, Any]) -> str | None:
for service in did_document.get("service", []):
if service.get("id") == "#atproto_pds":
return service.get("serviceEndpoint")
if service.get("type") == "AtprotoPersonalDataServer":
return service.get("serviceEndpoint")
return None
class AtProtoOAuthRedirect(OAuthRedirect):
"""AT Protocol OAuth redirect."""
client_class = AtProtoOAuthClient
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": ["atproto"],
}
class AtProtoOAuthCallback(OAuthCallback):
"""AT Protocol OAuth callback."""
client_class = AtProtoOAuthClient
def get_callback_url(self, source: OAuthSource) -> str:
return reverse(
"authentik_sources_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug},
)
def get_user_id(self, info: dict[str, Any]) -> str | None:
return info.get("did")
@registry.register()
class AtProtoType(SourceType):
"""AT Protocol Type definition"""
callback_view = AtProtoOAuthCallback
redirect_view = AtProtoOAuthRedirect
verbose_name = "AT Protocol"
name = "atproto"
# Defaults target Bluesky. They are editable because other AT Protocol
# authorization servers can expose the same endpoint roles on different URLs.
authorization_url = BSKY_AUTHORIZATION_URL_DEFAULT
request_token_url = BSKY_PAR_URL_DEFAULT
access_token_url = BSKY_TOKEN_URL_DEFAULT
profile_url = BSKY_PUBLIC_PROFILE_URL_DEFAULT
urls_customizable = True
pkce = PKCEMethod.S256
client_secret_required = False
def icon_url(self) -> str:
return static("authentik/sources/atproto.svg")
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return {
"username": info.get("handle") or info.get("did"),
"email": info.get("email"),
"name": info.get("displayName") or info.get("handle"),
}

View File

@@ -42,6 +42,8 @@ class SourceType:
oidc_jwks_url: str | None = None
pkce: PKCEMethod = PKCEMethod.NONE
client_secret_required = True
authorization_code_auth_method: AuthorizationCodeAuthMethod = (
AuthorizationCodeAuthMethod.BASIC_AUTH
)

View File

@@ -16,7 +16,7 @@ class RedirectMode(models.TextChoices):
class RedirectStage(Stage):
"""Redirect the user to a static URL or another flow, optionally with all gathered context."""
"""Redirect the user to another flow, potentially with all gathered context."""
keep_context = models.BooleanField(default=True)
mode = models.TextField(choices=RedirectMode.choices)

View File

@@ -7,7 +7,7 @@ from dramatiq.broker import Broker, MessageProxy, get_broker
from dramatiq.middleware.middleware import Middleware
from dramatiq.middleware.retries import Retries
from dramatiq.results.middleware import Results
from dramatiq.worker import ConsumerThread, Worker, WorkerThread
from dramatiq.worker import Worker, _ConsumerThread, _WorkerThread
from authentik.tasks.broker import PostgresBroker
@@ -20,7 +20,7 @@ class TestWorker(Worker):
self.worker_id = 1000
self.work_queue = PriorityQueue()
self.consumers = {
TESTING_QUEUE: ConsumerThread(
TESTING_QUEUE: _ConsumerThread(
broker=self.broker,
queue_name=TESTING_QUEUE,
prefetch=2,
@@ -33,7 +33,7 @@ class TestWorker(Worker):
prefetch=2,
timeout=1,
)
self._worker = WorkerThread(
self._worker = _WorkerThread(
broker=self.broker,
consumers=self.consumers,
work_queue=self.work_queue,
@@ -78,18 +78,17 @@ def use_test_broker():
actor.broker = broker
actor.broker.declare_actor(actor)
for middleware_class_path, middleware_kwargs in Conf().middlewares:
middleware_class = import_string(middleware_class_path)
if issubclass(middleware_class, Results):
middleware_kwargs["backend"] = import_string(Conf().result_backend)(
*Conf().result_backend_args,
**Conf().result_backend_kwargs,
)
middleware: Middleware = middleware_class(
for middleware_class, middleware_kwargs in Conf().middlewares:
middleware: Middleware = import_string(middleware_class)(
**middleware_kwargs,
)
if isinstance(middleware, Retries):
middleware.max_retries = 0
if isinstance(middleware, Results):
middleware.backend = import_string(Conf().result_backend)(
*Conf().result_backend_args,
**Conf().result_backend_kwargs,
)
broker.add_middleware(middleware)
broker.start()

View File

@@ -59,8 +59,6 @@ class FlagsJSONExtension(OpenApiSerializerFieldExtension):
props[_flag.key] = build_basic_type(get_args(_flag.__orig_bases__[0])[0])
if _flag.description:
props[_flag.key]["description"] = _flag.description
if _flag.deprecated:
props[_flag.key]["deprecated"] = _flag.deprecated
return build_object_type(props, required=props.keys())

View File

@@ -18,7 +18,6 @@ class Flag[T]:
Literal["none"] | Literal["public"] | Literal["authenticated"] | Literal["system"]
) = "none"
description: str | None = None
deprecated = False
def __init_subclass__(cls, key: str, **kwargs):
cls.__key = key

View File

@@ -1,211 +0,0 @@
# Minimal Invitation-based Enrollment Blueprint
#
# Companion to flows-invitation-enrollment.yaml, intended for the "New Invitation"
# wizard in the admin UI. Creates a single enrollment flow with an invitation stage
# bound to it, plus the supporting prompt/user-write/user-login stages.
#
# All user-facing fields are parameterized via !Context with fallback defaults, so
# this blueprint can be imported directly (without context) or through the wizard
# with custom values.
#
# Context keys (all optional):
# flow_name Display name of the enrollment flow.
# flow_slug URL slug of the flow and suffix for sub-entity
# identifiers (so repeated imports with different
# slugs don't overwrite each other).
# stage_name Name of the invitation stage.
# continue_flow_without_invitation Whether the flow continues when no invitation
# is supplied (default: false).
# user_type "external" or "internal" (default: "external").
# Drives the user-write stage's user_type and
# user_path_template.
version: 1
metadata:
labels:
blueprints.goauthentik.io/instantiate: "false"
name: Invitation-based Enrollment (minimal)
entries:
- identifiers:
slug: !Context [flow_slug, invitation-enrollment-flow]
model: authentik_flows.flow
id: flow
attrs:
name: !Context [flow_name, Invitation Enrollment Flow]
title: !Context [flow_name, Invitation Enrollment Flow]
designation: enrollment
authentication: require_unauthenticated
- identifiers:
name: !Context [stage_name, invitation-stage]
id: invitation-stage
model: authentik_stages_invitation.invitationstage
attrs:
continue_flow_without_invitation: !Context [continue_flow_without_invitation, false]
- identifiers:
name:
!Format [
"invitation-enrollment-field-username-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-username
model: authentik_stages_prompt.prompt
attrs:
field_key: username
label: Username
type: username
required: true
placeholder: Username
placeholder_expression: false
order: 0
- identifiers:
name:
!Format [
"invitation-enrollment-field-password-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-password
model: authentik_stages_prompt.prompt
attrs:
field_key: password
label: Password
type: password
required: true
placeholder: Password
placeholder_expression: false
order: 1
- identifiers:
name:
!Format [
"invitation-enrollment-field-password-repeat-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-password-repeat
model: authentik_stages_prompt.prompt
attrs:
field_key: password_repeat
label: Password (repeat)
type: password
required: true
placeholder: Password (repeat)
placeholder_expression: false
order: 2
- identifiers:
name:
!Format [
"invitation-enrollment-field-name-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-name
model: authentik_stages_prompt.prompt
attrs:
field_key: name
label: Name
type: text
required: true
placeholder: Name
placeholder_expression: false
order: 0
- identifiers:
name:
!Format [
"invitation-enrollment-field-email-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-email
model: authentik_stages_prompt.prompt
attrs:
field_key: email
label: Email
type: email
required: true
placeholder: Email
placeholder_expression: false
order: 1
- identifiers:
name:
!Format [
"invitation-enrollment-prompt-credentials-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-stage-credentials
model: authentik_stages_prompt.promptstage
attrs:
fields:
- !KeyOf prompt-field-username
- !KeyOf prompt-field-password
- !KeyOf prompt-field-password-repeat
- identifiers:
name:
!Format [
"invitation-enrollment-prompt-details-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-stage-details
model: authentik_stages_prompt.promptstage
attrs:
fields:
- !KeyOf prompt-field-name
- !KeyOf prompt-field-email
- identifiers:
name:
!Format [
"invitation-enrollment-user-write-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: user-write-stage
model: authentik_stages_user_write.userwritestage
attrs:
user_creation_mode: always_create
user_type: !Context [user_type, external]
user_path_template:
!Format ["users/%s", !Context [user_type, external]]
- identifiers:
name:
!Format [
"invitation-enrollment-user-login-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: user-login-stage
model: authentik_stages_user_login.userloginstage
- identifiers:
target: !KeyOf flow
stage: !KeyOf invitation-stage
order: 5
model: authentik_flows.flowstagebinding
attrs:
evaluate_on_plan: true
re_evaluate_policies: true
- identifiers:
target: !KeyOf flow
stage: !KeyOf prompt-stage-credentials
order: 10
model: authentik_flows.flowstagebinding
- identifiers:
target: !KeyOf flow
stage: !KeyOf prompt-stage-details
order: 15
model: authentik_flows.flowstagebinding
- identifiers:
target: !KeyOf flow
stage: !KeyOf user-write-stage
order: 20
model: authentik_flows.flowstagebinding
- identifiers:
target: !KeyOf flow
stage: !KeyOf user-login-stage
order: 100
model: authentik_flows.flowstagebinding

View File

@@ -12967,6 +12967,7 @@
"type": "string",
"enum": [
"apple",
"atproto",
"openidconnect",
"entraid",
"azuread",
@@ -13038,7 +13039,6 @@
},
"consumer_secret": {
"type": "string",
"minLength": 1,
"title": "Consumer secret"
},
"additional_scopes": {

View File

@@ -79,7 +79,7 @@ function prepare_debug {
apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc
source "${VENV_PATH}/bin/activate"
uv sync --active --locked
uv sync --active --frozen
touch /unittest.xml
chown authentik:authentik /unittest.xml
}

View File

@@ -200,7 +200,7 @@ RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=bind,target=packages/django-postgres-cache,src=packages/django-postgres-cache \
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
--mount=type=cache,id=uv-python-deps-$TARGETARCH$TARGETVARIANT,target=/root/.cache/uv \
uv sync --locked --no-install-project --no-dev
uv sync --frozen --no-install-project --no-dev
# Stage: Run
FROM python-base AS final-image

View File

@@ -28,7 +28,12 @@ class HttpHandler(BaseHTTPRequestHandler):
_ = db_conn.cursor()
def do_GET(self):
from django.db import DatabaseError, InterfaceError, OperationalError, connections
from django.db import (
DatabaseError,
InterfaceError,
OperationalError,
connections,
)
from psycopg.errors import AdminShutdown
from authentik.root.monitoring import monitoring_set
@@ -37,6 +42,7 @@ class HttpHandler(BaseHTTPRequestHandler):
AdminShutdown,
InterfaceError,
DatabaseError,
ConnectionError,
OperationalError,
)

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-06 00:27+0000\n"
"POT-Creation-Date: 2026-05-01 03:47+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -101,14 +101,6 @@ msgstr ""
msgid "Blueprint file does not exist"
msgstr ""
#: authentik/blueprints/api.py
msgid "Context must be valid JSON"
msgstr ""
#: authentik/blueprints/api.py
msgid "Context must be a JSON object"
msgstr ""
#: authentik/blueprints/api.py
msgid "Failed to validate blueprint"
msgstr ""

Binary file not shown.

View File

@@ -8,8 +8,7 @@
"url": "https://github.com/goauthentik/authentik.git"
},
"scripts": {
"build": "npm run clean && tsc -b tsconfig.json tsconfig.esm.json",
"clean": "tsc -b --clean tsconfig.json tsconfig.esm.json",
"build": "tsc && tsc -p tsconfig.esm.json",
"prepare": "npm run build"
},
"main": "./dist/index.js",

View File

@@ -47,7 +47,6 @@ export interface ManagedBlueprintsDestroyRequest {
export interface ManagedBlueprintsImportCreateRequest {
file?: Blob;
path?: string;
context?: string;
}
export interface ManagedBlueprintsListRequest {
@@ -370,10 +369,6 @@ export class ManagedApi extends runtime.BaseAPI {
formParams.append("path", requestParameters["path"] as any);
}
if (requestParameters["context"] != null) {
formParams.append("context", requestParameters["context"] as any);
}
let urlPath = `/managed/blueprints/import/`;
return {

View File

@@ -23,7 +23,7 @@ export interface AuthenticatedSessionUserAgentDevice {
* @type {string}
* @memberof AuthenticatedSessionUserAgentDevice
*/
brand: string | null;
brand: string;
/**
*
* @type {string}
@@ -35,7 +35,7 @@ export interface AuthenticatedSessionUserAgentDevice {
* @type {string}
* @memberof AuthenticatedSessionUserAgentDevice
*/
model: string | null;
model: string;
}
/**

View File

@@ -29,25 +29,25 @@ export interface AuthenticatedSessionUserAgentOs {
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
major: string | null;
major: string;
/**
*
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
minor: string | null;
minor: string;
/**
*
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
patch: string | null;
patch: string;
/**
*
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
patchMinor: string | null;
patchMinor: string;
}
/**

View File

@@ -40,7 +40,6 @@ export interface CurrentBrandFlags {
* Refresh other tabs after successful authentication.
* @type {boolean}
* @memberof CurrentBrandFlags
* @deprecated
*/
flowsRefreshOthers: boolean;
}

View File

@@ -162,7 +162,7 @@ export interface OAuthSourceRequest {
* @type {string}
* @memberof OAuthSourceRequest
*/
consumerSecret: string;
consumerSecret?: string;
/**
*
* @type {string}
@@ -203,7 +203,6 @@ export function instanceOfOAuthSourceRequest(value: object): value is OAuthSourc
if (!("slug" in value) || value["slug"] === undefined) return false;
if (!("providerType" in value) || value["providerType"] === undefined) return false;
if (!("consumerKey" in value) || value["consumerKey"] === undefined) return false;
if (!("consumerSecret" in value) || value["consumerSecret"] === undefined) return false;
return true;
}
@@ -252,7 +251,7 @@ export function OAuthSourceRequestFromJSONTyped(
profileUrl: json["profile_url"] == null ? undefined : json["profile_url"],
pkce: json["pkce"] == null ? undefined : PKCEMethodEnumFromJSON(json["pkce"]),
consumerKey: json["consumer_key"],
consumerSecret: json["consumer_secret"],
consumerSecret: json["consumer_secret"] == null ? undefined : json["consumer_secret"],
additionalScopes: json["additional_scopes"] == null ? undefined : json["additional_scopes"],
oidcWellKnownUrl:
json["oidc_well_known_url"] == null ? undefined : json["oidc_well_known_url"],

View File

@@ -40,7 +40,6 @@ export interface PatchedSettingsRequestFlags {
* Refresh other tabs after successful authentication.
* @type {boolean}
* @memberof PatchedSettingsRequestFlags
* @deprecated
*/
flowsRefreshOthers: boolean;
}

View File

@@ -18,6 +18,7 @@
*/
export const ProviderTypeEnum = {
Apple: "apple",
Atproto: "atproto",
Openidconnect: "openidconnect",
Entraid: "entraid",
Azuread: "azuread",

View File

@@ -14,8 +14,8 @@
import type { MatchingModeEnum } from "./MatchingModeEnum";
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
import type { RedirectURITypeEnum } from "./RedirectURITypeEnum";
import { RedirectURITypeEnumFromJSON, RedirectURITypeEnumToJSON } from "./RedirectURITypeEnum";
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
/**
* A single allowed redirect URI entry
@@ -37,10 +37,10 @@ export interface RedirectURI {
url: string;
/**
*
* @type {RedirectURITypeEnum}
* @type {RedirectUriTypeEnum}
* @memberof RedirectURI
*/
redirectUriType?: RedirectURITypeEnum;
redirectUriType?: RedirectUriTypeEnum;
}
/**
@@ -66,7 +66,7 @@ export function RedirectURIFromJSONTyped(json: any, ignoreDiscriminator: boolean
redirectUriType:
json["redirect_uri_type"] == null
? undefined
: RedirectURITypeEnumFromJSON(json["redirect_uri_type"]),
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
};
}
@@ -85,6 +85,6 @@ export function RedirectURIToJSONTyped(
return {
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
url: value["url"],
redirect_uri_type: RedirectURITypeEnumToJSON(value["redirectUriType"]),
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
};
}

View File

@@ -14,8 +14,8 @@
import type { MatchingModeEnum } from "./MatchingModeEnum";
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
import type { RedirectURITypeEnum } from "./RedirectURITypeEnum";
import { RedirectURITypeEnumFromJSON, RedirectURITypeEnumToJSON } from "./RedirectURITypeEnum";
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
/**
* A single allowed redirect URI entry
@@ -37,10 +37,10 @@ export interface RedirectURIRequest {
url: string;
/**
*
* @type {RedirectURITypeEnum}
* @type {RedirectUriTypeEnum}
* @memberof RedirectURIRequest
*/
redirectUriType?: RedirectURITypeEnum;
redirectUriType?: RedirectUriTypeEnum;
}
/**
@@ -69,7 +69,7 @@ export function RedirectURIRequestFromJSONTyped(
redirectUriType:
json["redirect_uri_type"] == null
? undefined
: RedirectURITypeEnumFromJSON(json["redirect_uri_type"]),
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
};
}
@@ -88,6 +88,6 @@ export function RedirectURIRequestToJSONTyped(
return {
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
url: value["url"],
redirect_uri_type: RedirectURITypeEnumToJSON(value["redirectUriType"]),
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
};
}

View File

@@ -1,57 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const RedirectURITypeEnum = {
Authorization: "authorization",
Logout: "logout",
UnknownDefaultOpenApi: "11184809",
} as const;
export type RedirectURITypeEnum = (typeof RedirectURITypeEnum)[keyof typeof RedirectURITypeEnum];
export function instanceOfRedirectURITypeEnum(value: any): boolean {
for (const key in RedirectURITypeEnum) {
if (Object.prototype.hasOwnProperty.call(RedirectURITypeEnum, key)) {
if (RedirectURITypeEnum[key as keyof typeof RedirectURITypeEnum] === value) {
return true;
}
}
}
return false;
}
export function RedirectURITypeEnumFromJSON(json: any): RedirectURITypeEnum {
return RedirectURITypeEnumFromJSONTyped(json, false);
}
export function RedirectURITypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RedirectURITypeEnum {
return json as RedirectURITypeEnum;
}
export function RedirectURITypeEnumToJSON(value?: RedirectURITypeEnum | null): any {
return value as any;
}
export function RedirectURITypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): RedirectURITypeEnum {
return value as RedirectURITypeEnum;
}

View File

@@ -0,0 +1,57 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const RedirectUriTypeEnum = {
Authorization: "authorization",
Logout: "logout",
UnknownDefaultOpenApi: "11184809",
} as const;
export type RedirectUriTypeEnum = (typeof RedirectUriTypeEnum)[keyof typeof RedirectUriTypeEnum];
export function instanceOfRedirectUriTypeEnum(value: any): boolean {
for (const key in RedirectUriTypeEnum) {
if (Object.prototype.hasOwnProperty.call(RedirectUriTypeEnum, key)) {
if (RedirectUriTypeEnum[key as keyof typeof RedirectUriTypeEnum] === value) {
return true;
}
}
}
return false;
}
export function RedirectUriTypeEnumFromJSON(json: any): RedirectUriTypeEnum {
return RedirectUriTypeEnumFromJSONTyped(json, false);
}
export function RedirectUriTypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RedirectUriTypeEnum {
return json as RedirectUriTypeEnum;
}
export function RedirectUriTypeEnumToJSON(value?: RedirectUriTypeEnum | null): any {
return value as any;
}
export function RedirectUriTypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): RedirectUriTypeEnum {
return value as RedirectUriTypeEnum;
}

View File

@@ -72,6 +72,12 @@ export interface SourceType {
* @memberof SourceType
*/
readonly oidcJwksUrl: string | null;
/**
*
* @type {boolean}
* @memberof SourceType
*/
clientSecretRequired: boolean;
}
/**
@@ -87,6 +93,8 @@ export function instanceOfSourceType(value: object): value is SourceType {
if (!("profileUrl" in value) || value["profileUrl"] === undefined) return false;
if (!("oidcWellKnownUrl" in value) || value["oidcWellKnownUrl"] === undefined) return false;
if (!("oidcJwksUrl" in value) || value["oidcJwksUrl"] === undefined) return false;
if (!("clientSecretRequired" in value) || value["clientSecretRequired"] === undefined)
return false;
return true;
}
@@ -108,6 +116,7 @@ export function SourceTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean)
profileUrl: json["profile_url"],
oidcWellKnownUrl: json["oidc_well_known_url"],
oidcJwksUrl: json["oidc_jwks_url"],
clientSecretRequired: json["client_secret_required"],
};
}
@@ -135,5 +144,6 @@ export function SourceTypeToJSONTyped(
name: value["name"],
verbose_name: value["verboseName"],
urls_customizable: value["urlsCustomizable"],
client_secret_required: value["clientSecretRequired"],
};
}

View File

@@ -707,7 +707,7 @@ export * from "./RedirectStageModeEnum";
export * from "./RedirectStageRequest";
export * from "./RedirectURI";
export * from "./RedirectURIRequest";
export * from "./RedirectURITypeEnum";
export * from "./RedirectUriTypeEnum";
export * from "./RelatedGroup";
export * from "./RelatedRule";
export * from "./Reputation";

View File

@@ -1,7 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"isolatedModules": true,
"incremental": true,
"rootDir": "src",
"strict": true,
"newLine": "lf",

View File

@@ -1,7 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"isolatedModules": true,
"incremental": true,
"rootDir": "src",
"strict": true,
"newLine": "lf",

View File

@@ -32,17 +32,16 @@ class DjangoDramatiqPostgres(AppConfig):
middleware=[],
)
for middleware_class_path, middleware_kwargs in Conf().middlewares:
middleware_class = import_string(middleware_class_path)
if issubclass(middleware_class, Results):
middleware_kwargs["backend"] = import_string(Conf().result_backend)(
for middleware_class, middleware_kwargs in Conf().middlewares:
middleware: dramatiq.middleware.middleware.Middleware = import_string(middleware_class)(
**middleware_kwargs,
)
if isinstance(middleware, Results):
middleware.backend = import_string(Conf().result_backend)(
*Conf().result_backend_args,
**Conf().result_backend_kwargs,
)
middleware: dramatiq.middleware.middleware.Middleware = middleware_class(
**middleware_kwargs,
)
broker.add_middleware(middleware)
broker.add_middleware(middleware) # type: ignore[no-untyped-call]
dramatiq.set_broker(broker)

View File

@@ -23,9 +23,11 @@ from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from dramatiq.broker import Broker, Consumer, MessageProxy
from dramatiq.common import compute_backoff, current_millis, dq_name, q_name, xq_name
from dramatiq.errors import BrokerConnectionError, QueueJoinTimeout
from dramatiq.errors import ConnectionError, QueueJoinTimeout
from dramatiq.message import Message
from dramatiq.middleware import Middleware
from dramatiq.middleware import (
Middleware,
)
from pglock.core import _cast_lock_id
from psycopg import sql
from psycopg.errors import AdminShutdown
@@ -44,6 +46,7 @@ DATABASE_ERRORS = (
AdminShutdown,
InterfaceError,
DatabaseError,
ConnectionError,
OperationalError,
)
@@ -52,7 +55,7 @@ def channel_name(queue_name: str, identifier: ChannelIdentifier) -> str:
return f"{CHANNEL_PREFIX}.{queue_name}.{identifier.value}"
def raise_broker_connection_error(func: Callable[P, R]) -> Callable[P, R]: # noqa: UP047
def raise_connection_error(func: Callable[P, R]) -> Callable[P, R]: # noqa: UP047
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
try:
@@ -63,13 +66,13 @@ def raise_broker_connection_error(func: Callable[P, R]) -> Callable[P, R]: # no
connections.close_all()
except DATABASE_ERRORS:
pass
raise BrokerConnectionError(str(exc)) from exc # type: ignore[no-untyped-call]
raise ConnectionError(str(exc)) from exc # type: ignore[no-untyped-call]
return wrapper
class PostgresBroker(Broker):
queues: set[str]
queues: set[str] # type: ignore[assignment]
def __init__(
self,
@@ -78,7 +81,7 @@ class PostgresBroker(Broker):
db_alias: str = DEFAULT_DB_ALIAS,
**kwargs: Any,
) -> None:
super().__init__(*args, middleware=[], **kwargs) # type: ignore[misc]
super().__init__(*args, middleware=[], **kwargs) # type: ignore[no-untyped-call,misc]
self.logger = get_logger(__name__, type(self))
self.queues = set()
@@ -119,10 +122,10 @@ class PostgresBroker(Broker):
def declare_queue(self, queue_name: str) -> None:
if queue_name not in self.queues:
self.emit_before("declare_queue", queue_name)
self.emit_before("declare_queue", queue_name) # type: ignore[no-untyped-call]
self.queues.add(queue_name)
# Nothing more to do, all queues are in the same table
self.emit_after("declare_queue", queue_name)
self.emit_after("declare_queue", queue_name) # type: ignore[no-untyped-call]
def model_defaults(self, message: Message[Any]) -> dict[str, Any]:
eta = None
@@ -138,7 +141,7 @@ class PostgresBroker(Broker):
}
@tenacity.retry(
retry=tenacity.retry_if_exception_type(BrokerConnectionError),
retry=tenacity.retry_if_exception_type(ConnectionError),
reraise=True,
wait=tenacity.wait_random_exponential(multiplier=1, max=5),
stop=tenacity.stop_after_attempt(3),
@@ -146,11 +149,11 @@ class PostgresBroker(Broker):
cast(logging.Logger, logger), logging.INFO, exc_info=True
),
)
@raise_broker_connection_error
@raise_connection_error
def enqueue(self, message: Message[Any], *, delay: int | None = None) -> Message[Any]:
queue_name = q_name(message.queue_name)
queue_name = q_name(message.queue_name) # type: ignore[no-untyped-call]
if delay:
message_eta = current_millis() + delay
message_eta = current_millis() + delay # type: ignore[no-untyped-call]
message.options["eta"] = message_eta
self.declare_queue(queue_name)
@@ -160,7 +163,7 @@ class PostgresBroker(Broker):
message.options["model_defaults"] = self.model_defaults(message)
message.options["model_create_defaults"] = {}
self.emit_before("enqueue", message, delay)
self.emit_before("enqueue", message, delay) # type: ignore[no-untyped-call]
with transaction.atomic(using=self.db_alias):
query = {
@@ -182,7 +185,7 @@ class PostgresBroker(Broker):
message.options["task"] = task
message.options["task_created"] = created
self.emit_after("enqueue", message, delay)
self.emit_after("enqueue", message, delay) # type: ignore[no-untyped-call]
return message
def get_declared_queues(self) -> set[str]:
@@ -190,7 +193,7 @@ class PostgresBroker(Broker):
def flush(self, queue_name: str) -> None:
self.query_set.filter(
queue_name__in=(queue_name, dq_name(queue_name), xq_name(queue_name))
queue_name__in=(queue_name, dq_name(queue_name), xq_name(queue_name)) # type: ignore[no-untyped-call]
).delete()
def flush_all(self) -> None:
@@ -372,7 +375,7 @@ class _PostgresConsumer(Consumer):
self.in_processing.add(str(message_id))
return message
@raise_broker_connection_error
@raise_connection_error
def __next__(self) -> MessageProxy | None:
# This method is called every second
@@ -392,7 +395,7 @@ class _PostgresConsumer(Consumer):
if processing >= self.prefetch:
# If we have too many messages already processing, wait and don't consume a message
# straight away, other workers will be faster.
self.misses, backoff_ms = compute_backoff(self.misses, max_backoff=1000)
self.misses, backoff_ms = compute_backoff(self.misses, max_backoff=1000) # type: ignore[no-untyped-call]
self.logger.debug(
"Too many messages in processing, Sleeping",
processing=processing,
@@ -417,7 +420,7 @@ class _PostgresConsumer(Consumer):
break
message = self._consume_one(str(message_id))
if message is not None:
return MessageProxy(message)
return MessageProxy(message) # type: ignore[no-untyped-call]
else:
self.logger.debug("Message already consumed. Skipping.", message_id=message_id)
continue
@@ -441,7 +444,7 @@ class _PostgresConsumer(Consumer):
self.to_unlock.add(str(message_id))
return False
def _post_process_message(self, message: MessageProxy, state: TaskState) -> None:
def _post_process_message(self, message: Message[Any], state: TaskState) -> None:
self.logger.debug("Post-processing message", message=message.message_id, state=state)
try:
self.in_processing.remove(str(message.message_id))
@@ -463,16 +466,16 @@ class _PostgresConsumer(Consumer):
)
message.options["task"] = task
@raise_broker_connection_error
def ack(self, message: MessageProxy) -> None:
@raise_connection_error
def ack(self, message: Message[Any]) -> None:
self._post_process_message(message, TaskState.DONE)
@raise_broker_connection_error
def nack(self, message: MessageProxy) -> None:
@raise_connection_error
def nack(self, message: Message[Any]) -> None:
self._post_process_message(message, TaskState.REJECTED)
@raise_broker_connection_error
def requeue(self, messages: Iterable[MessageProxy]) -> None:
@raise_connection_error
def requeue(self, messages: Iterable[Message[Any]]) -> None:
self.query_set.filter(
message_id__in=[message.message_id for message in messages],
).update(
@@ -511,7 +514,7 @@ class _PostgresConsumer(Consumer):
self.logger.info("Purged messages in all queues", count=count)
self.task_purge_last_run = timezone.now()
@raise_broker_connection_error
@raise_connection_error
def close(self) -> None:
try:
self._purge_locks()

View File

@@ -5,7 +5,7 @@ from signal import pause
from django_dramatiq_postgres.conf import Conf
def worker_metrics() -> int:
def worker_metrics() -> None:
import_module(Conf().autodiscovery["setup_module"])
from django_dramatiq_postgres.middleware import MetricsMiddleware
@@ -15,4 +15,3 @@ def worker_metrics() -> int:
int(os.getenv("dramatiq_prom_port", "9191")),
)
pause()
return 0

View File

@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, cast
from django.db import DatabaseError, close_old_connections, connections
from dramatiq.actor import Actor
from dramatiq.broker import Broker, MessageProxy
from dramatiq.broker import Broker
from dramatiq.common import current_millis
from dramatiq.message import Message
from dramatiq.middleware.middleware import Middleware
@@ -79,7 +79,7 @@ class DbConnectionMiddleware(Middleware):
class TaskStateBeforeMiddleware(Middleware):
def before_process_message(self, broker: PostgresBroker, message: Message[Any]) -> None: # type: ignore[override]
def before_process_message(self, broker: PostgresBroker, message: Message[Any]) -> None:
broker.query_set.filter(
message_id=message.message_id,
queue_name=message.queue_name,
@@ -90,7 +90,7 @@ class TaskStateBeforeMiddleware(Middleware):
class TaskStateAfterMiddleware(Middleware):
def before_process_message(self, broker: PostgresBroker, message: MessageProxy) -> None: # type: ignore[override]
def before_process_message(self, broker: PostgresBroker, message: Message[Any]) -> None:
broker.query_set.filter(
message_id=message.message_id,
queue_name=message.queue_name,
@@ -99,7 +99,7 @@ class TaskStateAfterMiddleware(Middleware):
state=TaskState.RUNNING,
)
def after_skip_message(self, broker: PostgresBroker, message: MessageProxy) -> None: # type: ignore[override]
def after_skip_message(self, broker: PostgresBroker, message: Message[Any]) -> None:
broker.query_set.filter(
message_id=message.message_id,
queue_name=message.queue_name,
@@ -110,11 +110,11 @@ class TaskStateAfterMiddleware(Middleware):
def after_process_message(
self,
broker: PostgresBroker, # type: ignore[override]
message: MessageProxy,
broker: PostgresBroker,
message: Message[Any],
*,
result: Any | None = None,
exception: BaseException | None = None,
exception: Exception | None = None,
) -> None:
self.after_skip_message(broker, message)
@@ -147,7 +147,7 @@ class CurrentTask(Middleware):
raise CurrentTaskNotFound()
return task[-1]
def before_process_message(self, broker: Broker, message: MessageProxy) -> None:
def before_process_message(self, broker: Broker, message: Message[Any]) -> None:
tasks = self._TASKS.get()
if tasks is None:
tasks = []
@@ -157,10 +157,10 @@ class CurrentTask(Middleware):
def after_process_message(
self,
broker: Broker,
message: MessageProxy,
message: Message[Any],
*,
result: Any | None = None,
exception: BaseException | None = None,
exception: Exception | None = None,
) -> None:
tasks: list[TaskBase] | None = self._TASKS.get()
if tasks is None or len(tasks) == 0:
@@ -194,7 +194,7 @@ class CurrentTask(Middleware):
pass
self._TASKS.set(tasks[:-1])
def after_skip_message(self, broker: Broker, message: MessageProxy) -> None:
def after_skip_message(self, broker: Broker, message: Message[Any]) -> None:
self.after_process_message(broker, message)
@@ -236,7 +236,7 @@ class MetricsMiddleware(Middleware):
self.message_start_times: dict[str, int] = {}
@property
def forks(self) -> list[Callable[[], int]]:
def forks(self) -> list[Callable[[], None]]:
from django_dramatiq_postgres.forks import worker_metrics
return [worker_metrics]
@@ -310,41 +310,41 @@ class MetricsMiddleware(Middleware):
# TODO: worker_id
multiprocess.mark_process_dead(os.getpid()) # type: ignore[no-untyped-call]
def _make_labels(self, message: MessageProxy | Message[Any]) -> list[str]:
def _make_labels(self, message: Message[Any]) -> list[str]:
return [message.queue_name, message.actor_name]
def after_nack(self, broker: Broker, message: MessageProxy) -> None:
def after_nack(self, broker: Broker, message: Message[Any]) -> None:
self.total_rejected_messages.labels(*self._make_labels(message)).inc()
def after_enqueue(self, broker: Broker, message: Message[Any], delay: int) -> None:
if "retries" in message.options:
self.total_retried_messages.labels(*self._make_labels(message)).inc()
def before_delay_message(self, broker: Broker, message: MessageProxy) -> None:
def before_delay_message(self, broker: Broker, message: Message[Any]) -> None:
self.delayed_messages.add(message.message_id)
self.in_progress_delayed_messages.labels(*self._make_labels(message)).inc()
def before_process_message(self, broker: Broker, message: MessageProxy) -> None:
def before_process_message(self, broker: Broker, message: Message[Any]) -> None:
labels = self._make_labels(message)
if message.message_id in self.delayed_messages:
self.delayed_messages.remove(message.message_id)
self.in_progress_delayed_messages.labels(*labels).dec()
self.in_progress_messages.labels(*labels).inc()
self.message_start_times[message.message_id] = current_millis()
self.message_start_times[message.message_id] = current_millis() # type: ignore[no-untyped-call]
def after_process_message(
self,
broker: Broker,
message: MessageProxy,
message: Message[Any],
*,
result: Any | None = None,
exception: BaseException | None = None,
exception: Exception | None = None,
) -> None:
labels = self._make_labels(message)
message_start_time = self.message_start_times.pop(message.message_id, current_millis())
message_duration = current_millis() - message_start_time
message_start_time = self.message_start_times.pop(message.message_id, current_millis()) # type: ignore[no-untyped-call]
message_duration = current_millis() - message_start_time # type: ignore[no-untyped-call]
self.messages_durations.labels(*labels).observe(message_duration)
self.in_progress_messages.labels(*labels).dec()

View File

@@ -159,7 +159,7 @@ class ScheduleBase(models.Model):
def send(self, broker: Broker | None = None) -> Message[Any]:
broker = broker or get_broker()
actor: Actor[Any, Any] = broker.get_actor(self.actor_name)
actor: Actor[Any, Any] = broker.get_actor(self.actor_name) # type: ignore[no-untyped-call]
return actor.send_with_options(
args=pickle.loads(self.args), # nosec
kwargs=pickle.loads(self.kwargs), # nosec

View File

@@ -36,7 +36,7 @@ dependencies = [
"django >=4.2,<6.0",
"django-pglock >=1.7,<2",
"django-pgtrigger >=4,<5",
"dramatiq >=2,<3",
"dramatiq >=1.17,<1.18",
"tenacity >=9,<10",
"structlog >=25,<26",
]

View File

@@ -9,7 +9,7 @@ dependencies = [
"argon2-cffi==25.1.0",
"cachetools==7.0.6",
"channels==4.3.2",
"cryptography==48.0.0",
"cryptography==47.0.0",
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
@@ -25,7 +25,7 @@ dependencies = [
"django-prometheus==2.4.1",
"django-storages[s3]==1.14.6",
"django-tenants==3.10.1",
"django==5.2.14",
"django==5.2.13",
"djangoql==0.19.1",
"djangorestframework==3.17.1",
"docker==7.1.0",
@@ -48,7 +48,7 @@ dependencies = [
"opencontainers==0.0.15",
"packaging==26.2",
"paramiko==4.0.0",
"psycopg[c,pool]==3.3.4",
"psycopg[c,pool]==3.3.3",
"pydantic-scim==0.0.8",
"pydantic==2.13.3",
"pyjwt==2.11.0",

View File

@@ -9678,14 +9678,18 @@ paths:
security:
- authentik: []
responses:
'200':
'204':
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintImportResult'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintImportResult'
description: ''
'403':
$ref: '#/components/responses/GenericErrorResponse'
/oauth2/access_tokens/:
@@ -34604,12 +34608,10 @@ components:
properties:
brand:
type: string
nullable: true
family:
type: string
model:
type: string
nullable: true
required:
- brand
- family
@@ -34622,16 +34624,12 @@ components:
type: string
major:
type: string
nullable: true
minor:
type: string
nullable: true
patch:
type: string
nullable: true
patch_minor:
type: string
nullable: true
required:
- family
- major
@@ -36046,8 +36044,6 @@ components:
path:
type: string
minLength: 1
context:
type: string
Brand:
type: object
description: Brand Serializer
@@ -37187,7 +37183,6 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -44648,7 +44643,6 @@ components:
consumer_secret:
type: string
writeOnly: true
minLength: 1
additional_scopes:
type: string
oidc_well_known_url:
@@ -44665,7 +44659,6 @@ components:
token request flow
required:
- consumer_key
- consumer_secret
- name
- provider_type
- slug
@@ -49974,7 +49967,6 @@ components:
consumer_secret:
type: string
writeOnly: true
minLength: 1
additional_scopes:
type: string
oidc_well_known_url:
@@ -51198,7 +51190,6 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -52681,6 +52672,7 @@ components:
ProviderTypeEnum:
enum:
- apple
- atproto
- openidconnect
- entraid
- azuread
@@ -53639,7 +53631,7 @@ components:
type: string
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectURITypeEnum'
- $ref: '#/components/schemas/RedirectUriTypeEnum'
default: authorization
required:
- matching_mode
@@ -53655,12 +53647,12 @@ components:
minLength: 1
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectURITypeEnum'
- $ref: '#/components/schemas/RedirectUriTypeEnum'
default: authorization
required:
- matching_mode
- url
RedirectURITypeEnum:
RedirectUriTypeEnum:
enum:
- authorization
- logout
@@ -55981,7 +55973,6 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -56070,7 +56061,6 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -56345,9 +56335,12 @@ components:
type: string
readOnly: true
nullable: true
client_secret_required:
type: boolean
required:
- access_token_url
- authorization_url
- client_secret_required
- name
- oidc_jwks_url
- oidc_well_known_url

View File

@@ -111,12 +111,8 @@ class TestFlowsEnroll(SeleniumTestCase):
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "a[data-ouia-component-id='enroll']"))
)
identification_stage.find_element(
By.CSS_SELECTOR, "a[data-ouia-component-id='enroll']"
).click()
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[ouiaId='enroll']")))
identification_stage.find_element(By.CSS_SELECTOR, "a[ouiaId='enroll']").click()
# First prompt stage
flow_executor = self.get_shadow_root("ak-flow-executor")

View File

@@ -27,14 +27,8 @@ class TestFlowsRecovery(SeleniumTestCase):
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(
ec.presence_of_element_located(
(By.CSS_SELECTOR, "a[data-ouia-component-id='recovery']")
)
)
identification_stage.find_element(
By.CSS_SELECTOR, "a[data-ouia-component-id='recovery']"
).click()
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[ouiaId='recovery']")))
identification_stage.find_element(By.CSS_SELECTOR, "a[ouiaId='recovery']").click()
# First prompt stage
flow_executor = self.get_shadow_root("ak-flow-executor")

127
uv.lock generated
View File

@@ -318,11 +318,11 @@ requires-dist = [
{ name = "argon2-cffi", specifier = "==25.1.0" },
{ name = "cachetools", specifier = "==7.0.6" },
{ name = "channels", specifier = "==4.3.2" },
{ name = "cryptography", specifier = "==48.0.0" },
{ name = "cryptography", specifier = "==47.0.0" },
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "django", specifier = "==5.2.14" },
{ name = "django", specifier = "==5.2.13" },
{ name = "django-channels-postgres", editable = "packages/django-channels-postgres" },
{ name = "django-countries", specifier = "==8.2.0" },
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
@@ -357,7 +357,7 @@ requires-dist = [
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
{ name = "packaging", specifier = "==26.2" },
{ name = "paramiko", specifier = "==4.0.0" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.4" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.3" },
{ name = "pydantic", specifier = "==2.13.3" },
{ name = "pydantic-scim", specifier = "==0.0.8" },
{ name = "pyjwt", specifier = "==2.11.0" },
@@ -917,55 +917,55 @@ wheels = [
[[package]]
name = "cryptography"
version = "48.0.0"
version = "47.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
{ url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" },
{ url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" },
{ url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" },
{ url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" },
{ url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" },
{ url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" },
{ url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" },
{ url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" },
{ url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" },
{ url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" },
{ url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" },
{ url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" },
{ url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" },
{ url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" },
{ url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" },
{ url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" },
{ url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" },
{ url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" },
{ url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" },
{ url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" },
{ url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" },
{ url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" },
{ url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" },
{ url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" },
{ url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" },
{ url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" },
{ url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" },
{ url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" },
{ url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" },
{ url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" },
{ url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" },
{ url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" },
{ url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" },
{ url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" },
{ url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" },
]
[[package]]
@@ -1075,16 +1075,16 @@ wheels = [
[[package]]
name = "django"
version = "5.2.14"
version = "5.2.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/65/95/95f7faa0950867afaa0bef2460c6263afd6a2c78cc9434046ed28160b015/django-5.2.14.tar.gz", hash = "sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2", size = 10895118, upload-time = "2026-05-05T13:57:31.104Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/44/f172870cf87aa25afef48fb72adba89ee8b77fcab6f3b23d240b923f1528/django-5.2.14-py3-none-any.whl", hash = "sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76", size = 8311320, upload-time = "2026-05-05T13:57:25.795Z" },
{ url = "https://files.pythonhosted.org/packages/59/b1/51ab36b2eefcf8cdb9338c7188668a157e29e30306bfc98a379704c9e10d/django-5.2.13-py3-none-any.whl", hash = "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", size = 8310982, upload-time = "2026-04-07T14:02:08.883Z" },
]
[[package]]
@@ -1143,7 +1143,7 @@ requires-dist = [
{ name = "django", specifier = ">=4.2,<6.0" },
{ name = "django-pglock", specifier = ">=1.7,<2" },
{ name = "django-pgtrigger", specifier = ">=4,<5" },
{ name = "dramatiq", specifier = ">=2,<3" },
{ name = "dramatiq", specifier = ">=1.17,<1.18" },
{ name = "structlog", specifier = ">=25,<26" },
{ name = "tenacity", specifier = ">=9,<10" },
]
@@ -1381,11 +1381,14 @@ wheels = [
[[package]]
name = "dramatiq"
version = "2.1.0"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/69/02b54e3fc4fe75721b322bc578054b4f03cec258ba614fa98a1a5bbe1efe/dramatiq-2.1.0.tar.gz", hash = "sha256:cf81550729de6cf64234b05bd63970645654aaf38967faa7a2b6e401384bb090", size = 105444, upload-time = "2026-03-03T11:22:10.067Z" }
dependencies = [
{ name = "prometheus-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/7a/6792ddc64a77d22bfd97261b751a7a76cf2f9d62edc59aafb679ac48b77d/dramatiq-1.17.1.tar.gz", hash = "sha256:2675d2f57e0d82db3a7d2a60f1f9c536365349db78c7f8d80a63e4c54697647a", size = 99071, upload-time = "2024-10-26T05:09:28.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/91/422960c8c415fd31ca1519d71d6f7e4bcabb2cdcc5872f784467e9fe7237/dramatiq-2.1.0-py3-none-any.whl", hash = "sha256:3ef940c2815722d3679aed79ef96c805f02fd33d4361529b2de30f01511ca44d", size = 125543, upload-time = "2026-03-03T11:22:08.664Z" },
{ url = "https://files.pythonhosted.org/packages/ee/36/925c7afd5db4f1a3f00676b9c3c58f31ff7ae29a347282d86c8d429280a5/dramatiq-1.17.1-py3-none-any.whl", hash = "sha256:951cdc334478dff8e5150bb02a6f7a947d215ee24b5aedaf738eff20e17913df", size = 120382, upload-time = "2024-10-26T05:09:26.436Z" },
]
[[package]]
@@ -2730,14 +2733,14 @@ wheels = [
[[package]]
name = "psycopg"
version = "3.3.4"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
]
[package.optional-dependencies]
@@ -2750,9 +2753,9 @@ pool = [
[[package]]
name = "psycopg-c"
version = "3.3.4"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/7c/c08364f2eab2913e4068b3b955d963e7a3491986a85429990969525def30/psycopg_c-3.3.4.tar.gz", hash = "sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22", size = 647111, upload-time = "2026-05-01T23:31:58.032Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/a0/8feb0ca8c7c20a8b9ac4d46b335ddd57e48e593b714262f006880f34fee5/psycopg_c-3.3.3.tar.gz", hash = "sha256:86ef6f4424348247828e83fb0882c9f8acb33e64d0a5ce66c1b4a5107ee73edd", size = 631965, upload-time = "2026-02-18T16:52:18.084Z" }
[[package]]
name = "psycopg-pool"
@@ -2944,14 +2947,14 @@ wheels = [
[[package]]
name = "pyopenssl"
version = "26.2.0"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/a8/26d36401e3ab8eed9030ad33f381da7856fcfad5691780fccd1b019718fc/pyopenssl-26.1.0.tar.gz", hash = "sha256:737f0a2275c5bc54f3b02137687e1a765931fb3949b9a92a825e4d33b9eec08b", size = 186181, upload-time = "2026-04-24T20:23:48.115Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
{ url = "https://files.pythonhosted.org/packages/a8/41/52f3a3e812b816a91e89aa504199d8bf989a1f873192b10762be66cf2009/pyopenssl-26.1.0-py3-none-any.whl", hash = "sha256:115563879b2c8ccb207975705d3e491434d8c9d7c79667c902ecbf5f3bbd2ece", size = 58109, upload-time = "2026-04-24T20:23:46.273Z" },
]
[[package]]

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
<path fill="#1185fe" d="M320 291.1C293.9 240.4 222.9 145.9 156.9 99.3C93.6 54.6 69.5 62.3 53.6 69.5C35.3 77.8 32 105.9 32 122.4C32 138.9 41.1 258 47 277.9C66.5 343.6 136.1 365.8 200.2 358.6C106.3 372.6 22.9 406.8 132.3 528.5C252.6 653.1 297.1 501.8 320 425.1C342.9 501.8 369.2 647.6 505.6 528.5C608 425.1 533.7 372.5 439.8 358.6C503.9 365.7 573.4 343.5 593 277.9C598.9 258 608 139 608 122.4C608 105.8 604.7 77.7 586.4 69.5C570.6 62.4 546.4 54.6 483.2 99.3C417.1 145.9 346.1 240.4 320 291.1Z"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

424
web/package-lock.json generated
View File

@@ -44,10 +44,10 @@
"@patternfly/patternfly": "^4.224.2",
"@playwright/test": "^1.59.1",
"@sentry/browser": "^10.50.0",
"@storybook/addon-docs": "^10.3.6",
"@storybook/addon-links": "^10.3.6",
"@storybook/web-components": "^10.3.6",
"@storybook/web-components-vite": "^10.3.6",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-links": "^10.3.5",
"@storybook/web-components": "^10.3.5",
"@storybook/web-components-vite": "^10.3.5",
"@types/codemirror": "^5.60.17",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.5",
@@ -66,7 +66,7 @@
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.2",
"core-js": "^3.49.0",
"country-flag-icons": "^1.6.17",
"country-flag-icons": "^1.6.16",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.4.2",
@@ -114,11 +114,11 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.57.2",
"unist-util-visit": "^5.1.0",
"vite": "^8.0.10",
"vite": "^8.0.8",
"vitest": "^4.1.1",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",
"yaml": "^2.8.4"
"yaml": "^2.8.3"
},
"engines": {
"node": ">=24",
@@ -2895,9 +2895,9 @@
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
"cpu": [
"arm64"
],
@@ -2911,9 +2911,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
"cpu": [
"arm64"
],
@@ -2927,9 +2927,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
"cpu": [
"x64"
],
@@ -2943,9 +2943,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
"cpu": [
"x64"
],
@@ -2959,9 +2959,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
"cpu": [
"arm"
],
@@ -2975,9 +2975,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
"cpu": [
"arm64"
],
@@ -2991,9 +2991,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
"cpu": [
"arm64"
],
@@ -3007,9 +3007,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
"cpu": [
"ppc64"
],
@@ -3023,9 +3023,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
"cpu": [
"s390x"
],
@@ -3039,9 +3039,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
"cpu": [
"x64"
],
@@ -3055,9 +3055,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
"cpu": [
"x64"
],
@@ -3071,9 +3071,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
"cpu": [
"arm64"
],
@@ -3087,48 +3087,27 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.3"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
"cpu": [
"arm64"
],
@@ -3142,9 +3121,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"cpu": [
"x64"
],
@@ -3158,9 +3137,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": {
@@ -3719,15 +3698,15 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.6.tgz",
"integrity": "sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.5.tgz",
"integrity": "sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==",
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.3.6",
"@storybook/csf-plugin": "10.3.5",
"@storybook/icons": "^2.0.1",
"@storybook/react-dom-shim": "10.3.6",
"@storybook/react-dom-shim": "10.3.5",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -3737,13 +3716,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.3.6"
"storybook": "^10.3.5"
}
},
"node_modules/@storybook/addon-links": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.6.tgz",
"integrity": "sha512-tv9Xd68qRGBAvEubaxNo3FuFq4GwuMiBriD+gLGuFK0+/u3cnkuA264aoR1v6YCH3sT3er3+MBimuyKM3jLDxg==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.5.tgz",
"integrity": "sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==",
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -3754,7 +3733,7 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.3.6"
"storybook": "^10.3.5"
},
"peerDependenciesMeta": {
"react": {
@@ -3763,12 +3742,12 @@
}
},
"node_modules/@storybook/builder-vite": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.6.tgz",
"integrity": "sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.5.tgz",
"integrity": "sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==",
"license": "MIT",
"dependencies": {
"@storybook/csf-plugin": "10.3.6",
"@storybook/csf-plugin": "10.3.5",
"ts-dedent": "^2.0.0"
},
"funding": {
@@ -3776,14 +3755,14 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.3.6",
"storybook": "^10.3.5",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@storybook/csf-plugin": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.6.tgz",
"integrity": "sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.5.tgz",
"integrity": "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==",
"license": "MIT",
"dependencies": {
"unplugin": "^2.3.5"
@@ -3795,7 +3774,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.3.6",
"storybook": "^10.3.5",
"vite": "*",
"webpack": "*"
},
@@ -3831,9 +3810,9 @@
}
},
"node_modules/@storybook/react-dom-shim": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.6.tgz",
"integrity": "sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.5.tgz",
"integrity": "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -3842,13 +3821,13 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.3.6"
"storybook": "^10.3.5"
}
},
"node_modules/@storybook/web-components": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.6.tgz",
"integrity": "sha512-femDZGYBGQDckL7F6ZCl2S+dNNBjvd9lp6rQrwBdbNprjctLd6d3EB4HyNM502QxtdEo7laq8y1goDu8KwIV3A==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.5.tgz",
"integrity": "sha512-tSppZagFCeZ+bWsaHUvdiw17ATWgfGDBz0mFicgEj0/eNuxQH2OvXyRIQUXY39b/55TBwSGeoIX3tOW91WIqpw==",
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -3861,24 +3840,24 @@
},
"peerDependencies": {
"lit": "^2.0.0 || ^3.0.0",
"storybook": "^10.3.6"
"storybook": "^10.3.5"
}
},
"node_modules/@storybook/web-components-vite": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.6.tgz",
"integrity": "sha512-VeDEAJuOOQV6VAqEF0pilXucS6kp+1ILJVkI+ets6Ku2D+RKeu167YrQAzh1NwzRTv0e5H0anDDNke+sWvg2dg==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.5.tgz",
"integrity": "sha512-6uAw6KAUXFsAPzp8KchcMp3gatEnEAd8ylIvzoMzvsIMiHmzXwvDNmoFZnAJ2tmsQGvF4dZRDCBg7PvWdTx8Rg==",
"license": "MIT",
"dependencies": {
"@storybook/builder-vite": "10.3.6",
"@storybook/web-components": "10.3.6"
"@storybook/builder-vite": "10.3.5",
"@storybook/web-components": "10.3.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.3.6"
"storybook": "^10.3.5"
}
},
"node_modules/@swagger-api/apidom-ast": {
@@ -4577,9 +4556,9 @@
}
},
"node_modules/@swc/core": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.32.tgz",
"integrity": "sha512-/eWL0n43D64QWEUHLtTE+jDqjkJhyidjkDhv6f0uJohOUAhywxQ9wXYp845DNNds0JpCdI4Uo0a9bl+vbXf+ew==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4594,18 +4573,18 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-darwin-x64": "1.15.33",
"@swc/core-linux-arm-gnueabihf": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-arm64-musl": "1.15.33",
"@swc/core-linux-ppc64-gnu": "1.15.33",
"@swc/core-linux-s390x-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/core-linux-x64-musl": "1.15.33",
"@swc/core-win32-arm64-msvc": "1.15.33",
"@swc/core-win32-ia32-msvc": "1.15.33",
"@swc/core-win32-x64-msvc": "1.15.33"
"@swc/core-darwin-arm64": "1.15.32",
"@swc/core-darwin-x64": "1.15.32",
"@swc/core-linux-arm-gnueabihf": "1.15.32",
"@swc/core-linux-arm64-gnu": "1.15.32",
"@swc/core-linux-arm64-musl": "1.15.32",
"@swc/core-linux-ppc64-gnu": "1.15.32",
"@swc/core-linux-s390x-gnu": "1.15.32",
"@swc/core-linux-x64-gnu": "1.15.32",
"@swc/core-linux-x64-musl": "1.15.32",
"@swc/core-win32-arm64-msvc": "1.15.32",
"@swc/core-win32-ia32-msvc": "1.15.32",
"@swc/core-win32-x64-msvc": "1.15.32"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
@@ -4617,9 +4596,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz",
"integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.32.tgz",
"integrity": "sha512-/YWMvJDPu+AAwuUsM2G+DNQ/7zhodURGzdQyewEqcvgklAdDHs3LwQmLLnyn6SJl8DT8UOxkbzK+D1PmPeelRg==",
"cpu": [
"arm64"
],
@@ -4633,9 +4612,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz",
"integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.32.tgz",
"integrity": "sha512-KOTXJXdAhWL+hZ77MYP3z+4pcMFaQhQ74yqyN1uz093q0YnbxpqMtYpPISbYvMHzVRNNx5kN+9RZAXEaadhWVA==",
"cpu": [
"x64"
],
@@ -4649,9 +4628,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz",
"integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.32.tgz",
"integrity": "sha512-oOoxLweljlc0A4X8ybsgxV7cVaYTwBOg2iMDJcFR3Sr48C+lsv9VzSmqdK/IVIXF4W4GjLc3VqTAdSMXlfVLuQ==",
"cpu": [
"arm"
],
@@ -4665,9 +4644,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz",
"integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.32.tgz",
"integrity": "sha512-oDzEkdl6D6BAWdMtU5KGO7y3HR5fJcvByNLyEk9+ugj8nP5Ovb7P4kBcStBXc4MPExFGQryehiINMlmY8HlclA==",
"cpu": [
"arm64"
],
@@ -4681,9 +4660,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz",
"integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.32.tgz",
"integrity": "sha512-omcqjoZP/b8D8PuczVoRwJieC6ibj7qIxTftNYokz4/aSmKFHvsd7nIFfPk5ZvtzncbH4AY7+Dkr/Lp2gWxYeA==",
"cpu": [
"arm64"
],
@@ -4697,9 +4676,9 @@
}
},
"node_modules/@swc/core-linux-ppc64-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz",
"integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.32.tgz",
"integrity": "sha512-KGkTMyz/Tbn3PBNu0AVZ4GTDFKnICrYcTiNPZq8DrvK42pnFsf3GNDrIG9E5AtQlTmC0YigkWKmu0eMcfTrmgA==",
"cpu": [
"ppc64"
],
@@ -4713,9 +4692,9 @@
}
},
"node_modules/@swc/core-linux-s390x-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz",
"integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.32.tgz",
"integrity": "sha512-G3Aa4tVS/3OGZBkoNIwUF9F6RAy+Osb4GOlo62SinLmDiErz/ykmM7KH0wkz6l9kM8jJq1HyAM6atJTUEbBk7g==",
"cpu": [
"s390x"
],
@@ -4729,9 +4708,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz",
"integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.32.tgz",
"integrity": "sha512-ERsjfGcj6CBmj3vJnGDO8m8rTvw6RqMcWo1dogOtNx3/+/0+NNpJiXDobJrr1GwInI/BHAEkvSFIH6d2LqPcUQ==",
"cpu": [
"x64"
],
@@ -4745,9 +4724,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz",
"integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.32.tgz",
"integrity": "sha512-N4Ggahe/8SUbTX50P6EdhbW9YWcgbZVb52R4cq6MK+zsoMjRq7rGvV5ztA05QnbaCYqMYx8rTY7KAIA3Crdo4Q==",
"cpu": [
"x64"
],
@@ -4761,9 +4740,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz",
"integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.32.tgz",
"integrity": "sha512-01yN0o9jvo8xBTP12aPK2wW8b41jmOlGbDDlAnoynotc4pO6xA0zby9f1z6j++qXDpGBttLySq1omgVrlQKYcw==",
"cpu": [
"arm64"
],
@@ -4777,9 +4756,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz",
"integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.32.tgz",
"integrity": "sha512-fLagI9XZYNpTcmlqAcp3KBtmj7E19WCmYD80Jxj1Kn5tGNa7yxNLd3NNdWxuZGUPl5iC0/KqZru7g08gF6Fsrw==",
"cpu": [
"ia32"
],
@@ -4793,9 +4772,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz",
"integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==",
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.32.tgz",
"integrity": "sha512-gbc2bQ/T2CiR+w0OvcVKwLOFAcPZBvmWmolbwpg1E8UrpeC03DGtyMUApOHNXNYWA3SHFrYXCQtosrcMza1YFg==",
"cpu": [
"x64"
],
@@ -6548,12 +6527,12 @@
}
},
"node_modules/axios": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
@@ -7475,9 +7454,9 @@
}
},
"node_modules/country-flag-icons": {
"version": "1.6.17",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.17.tgz",
"integrity": "sha512-Nmik0289ZVZSI3c7mJR/amg6DyY7Z59b0sTFSKayeX72mHfPzCPJygwJs2pYgQULzuAyWeCUgwAJ+Dq8OR+JFw==",
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.16.tgz",
"integrity": "sha512-HxJVoE/aaZGcUMx1vK/u9430uKGB3ODZDDZJJOqVJQzoHk5v42c0fSp1rk4tDfyr1dVOJjwxRiaBPliBMo2Liw==",
"license": "MIT"
},
"node_modules/crelt": {
@@ -10748,9 +10727,9 @@
}
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"optional": true,
"engines": {
@@ -15832,13 +15811,13 @@
"license": "Unlicense"
},
"node_modules/rolldown": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.127.0",
"@rolldown/pluginutils": "1.0.0-rc.17"
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -15847,21 +15826,30 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
}
},
"node_modules/rolldown/node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/rollup": {
@@ -16610,9 +16598,9 @@
}
},
"node_modules/storybook": {
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.6.tgz",
"integrity": "sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==",
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.5.tgz",
"integrity": "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==",
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -16637,15 +16625,11 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"prettier": "^2 || ^3",
"vite-plus": "^0.1.15"
"prettier": "^2 || ^3"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
},
"vite-plus": {
"optional": true
}
}
},
@@ -18416,16 +18400,16 @@
}
},
"node_modules/vite": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.10",
"rolldown": "1.0.0-rc.17",
"tinyglobby": "^0.2.16"
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
@@ -18506,22 +18490,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/vite/node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vitest": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz",
@@ -19093,9 +19061,9 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz",
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -19280,7 +19248,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.15.33",
"@swc/core": "^1.15.32",
"@webcomponents/template": "^1.5.1",
"base64-js": "^1.5.1",
"core-js": "^3.49.0",

View File

@@ -120,10 +120,10 @@
"@patternfly/patternfly": "^4.224.2",
"@playwright/test": "^1.59.1",
"@sentry/browser": "^10.50.0",
"@storybook/addon-docs": "^10.3.6",
"@storybook/addon-links": "^10.3.6",
"@storybook/web-components": "^10.3.6",
"@storybook/web-components-vite": "^10.3.6",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-links": "^10.3.5",
"@storybook/web-components": "^10.3.5",
"@storybook/web-components-vite": "^10.3.5",
"@types/codemirror": "^5.60.17",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.5",
@@ -142,7 +142,7 @@
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.2",
"core-js": "^3.49.0",
"country-flag-icons": "^1.6.17",
"country-flag-icons": "^1.6.16",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.4.2",
@@ -190,11 +190,11 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.57.2",
"unist-util-visit": "^5.1.0",
"vite": "^8.0.10",
"vite": "^8.0.8",
"vitest": "^4.1.1",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",
"yaml": "^2.8.4"
"yaml": "^2.8.3"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.28.0",

View File

@@ -7,186 +7,152 @@
*/
/**
* A token produced by a {@link LexerAction}. The lexer is agnostic to the
* concrete token shape; consumers pick whatever representation suits them.
*
* @typedef {unknown} Token
* @typedef {(this: Lexer, chr: string) => any} DefunctFunction
*/
/**
* A rule action. Invoked with the regex match (full match followed by capture
* groups) bound to the owning {@link Lexer} so it can read or set `state`,
* `index`, and `reject`.
*
* Return values:
* - `null` (or `undefined` from an implicit return) — discard the match and continue scanning.
* - a single token — yield it from {@link Lexer.lex}.
* - an array of tokens — yield the first; queue the rest for subsequent calls.
*
* @callback LexerAction
* @this {Lexer}
* @param {...string} match
* @returns {Token | Token[] | null | void}
* @typedef {(this: Lexer, ...args: RegExpExecArray) => string | string[] | undefined} RuleAction
*/
/**
* @typedef {object} LexerRule
* @property {RegExp} pattern Sticky-compiled pattern used to probe the input.
* @property {boolean} global Whether the user-supplied pattern was global.
* @property {LexerAction} action
* @property {number[]} start States in which the rule is active. `[0]` is the default state; an empty array means "any state".
* @typedef {Object} Rule
* @property {RegExp} pattern
* @property {boolean} global
* @property {RuleAction} action
* @property {number[]} start
*/
/**
* @typedef {object} LexerMatch
* @typedef {Object} Match
* @property {RegExpExecArray} result
* @property {LexerAction} action
* @property {RuleAction} action
* @property {number} length
* @property {boolean} global Whether the producing rule was declared with the `g` flag.
*/
/**
* Handler invoked when no rule matches at the current position.
*
* @callback DefunctHandler
* @this {Lexer}
* @param {string} chr The unexpected character.
* @returns {Token | Token[] | null | void}
*/
/**
* @type {DefunctHandler}
*/
function defaultDefunct(chr) {
throw new Error(`Unexpected character at index ${this.index - 1}: ${chr}`);
}
/**
* Lexer class for tokenizing input strings.
*/
export class Lexer {
/**
* Current lexer state. Rules whose `start` array contains this value (or
* is empty) are eligible to match. Odd-numbered states are also matched
* by rules declared with `start: [0]`, mirroring flex's inclusive states.
*
* @type {string[]}
*/
tokens = [];
/**
* @type {Rule[]}
*/
rules = [];
/**
* @type {number}
*/
remove = 0;
/**
* @type {number}
*/
state = 0;
/** @type {number} */
/**
* @type {number}
*/
index = 0;
/** @type {string} */
/**
* @type {string}
*/
input = "";
/**
* When set to `true` from inside an action, the current match is rolled
* back and the next-best match is tried instead.
*
* @type {boolean}
*/
reject = false;
/** @type {LexerRule[]} */
#rules = [];
/** @type {Token[]} */
#tokens = [];
/** @type {number} */
#remove = 0;
/** @type {DefunctHandler} */
#defunct;
/**
* @param {DefunctHandler} [defunct] Optional handler for unexpected characters.
* @param {DefunctFunction} [defunct]
*/
constructor(defunct) {
this.#defunct = typeof defunct === "function" ? defunct : defaultDefunct;
defunct ||= function (chr) {
throw new Error("Unexpected character at index " + (this.index - 1) + ": " + chr);
};
this.defunct = defunct;
}
/**
* Register a tokenization rule.
* Add a lexing rule.
*
* @param {RegExp} pattern
* @param {LexerAction} action
* @param {number[]} [start] States in which the rule is active. Defaults to `[0]`.
* @returns {this}
* @param {RuleAction} action
* @param {number[]} [start]
* @returns {Lexer}
*/
addRule(pattern, action, start) {
addRule = (pattern, action, start) => {
const global = pattern.global;
if (!global || !pattern.sticky) {
let flags = "gy";
if (pattern.multiline) flags += "m";
if (pattern.ignoreCase) flags += "i";
if (pattern.unicode) flags += "u";
pattern = new RegExp(pattern.source, flags);
}
this.#rules.push({
pattern,
global,
action,
start: Array.isArray(start) ? start : [0],
if (!Array.isArray(start)) start = [0];
this.rules.push({
pattern: pattern,
global: global,
action: action,
start: start,
});
return this;
}
};
/**
* Reset the lexer and load a new input string.
* Set the input string for lexing.
*
* @param {string} input
* @returns {this}
* @returns {Lexer}
*/
setInput(input) {
this.#remove = 0;
setInput = (input) => {
this.remove = 0;
this.state = 0;
this.index = 0;
this.#tokens.length = 0;
this.tokens.length = 0;
this.input = input;
return this;
}
};
/**
* Produce the next token from the input, or `null` once exhausted.
* Lex the next token from the input.
*
* @returns {Token | null}
* @returns {string | string[] | undefined}
*/
lex() {
if (this.#tokens.length) return /** @type {Token} */ (this.#tokens.shift());
lex = () => {
if (this.tokens.length) return this.tokens.shift();
this.reject = true;
while (this.index <= this.input.length) {
const matches = this.#scan().splice(this.#remove);
const matches = this.scan().splice(this.remove);
const index = this.index;
while (matches.length) {
if (!this.reject) break;
if (!this.reject) {
break;
}
const match = matches.shift();
const match = /** @type {LexerMatch} */ (matches.shift());
const { result, length } = match;
if (!match) break;
const result = match.result;
const length = match.length;
this.index += length;
this.reject = false;
this.#remove++;
this.remove++;
let token = match.action.apply(
this,
/** @type {string[]} */ (/** @type {unknown} */ (result)),
);
let token = match.action.apply(this, result);
if (this.reject) {
this.index = result.index;
} else if (token !== null && token !== undefined) {
if (Array.isArray(token)) {
this.#tokens = token.slice(1);
token = token[0];
}
if (length) this.#remove = 0;
} else if (Array.isArray(token)) {
this.tokens = token.slice(1);
token = token[0];
} else {
if (length) this.remove = 0;
return token;
}
}
@@ -195,82 +161,79 @@ export class Lexer {
if (index < input.length) {
if (this.reject) {
this.#remove = 0;
const token = this.#defunct(input.charAt(this.index++));
if (token !== null && token !== undefined) {
this.remove = 0;
const token = this.defunct(input.charAt(this.index++));
if (typeof token !== "undefined") {
if (Array.isArray(token)) {
this.#tokens = token.slice(1);
this.tokens = token.slice(1);
return token[0];
}
return token;
}
} else {
if (this.index !== index) this.#remove = 0;
if (this.index !== index) this.remove = 0;
this.reject = true;
}
} else if (matches.length) {
this.reject = true;
} else {
break;
}
} else if (matches.length) this.reject = true;
else break;
}
return null;
}
};
/**
* Probe every state-eligible rule at the current position, returning the
* matches sorted by length (longest first), with global rules pinned
* after non-global ones to preserve flex's "longest non-global wins"
* tie-breaking.
* Scan the input for matches.
*
* @returns {LexerMatch[]}
* @returns {Match[]}
*/
#scan() {
/** @type {LexerMatch[]} */
scan = () => {
/**
* @type {Match[]}
*/
const matches = [];
let index = 0;
const state = this.state;
const lastIndex = this.index;
const input = this.input;
for (const rule of this.#rules) {
for (let i = 0, length = this.rules.length; i < length; i++) {
const rule = this.rules[i];
const start = rule.start;
const states = start.length;
const eligible =
!states || start.indexOf(state) >= 0 || (state % 2 && states === 1 && !start[0]);
if (!eligible) continue;
if (!states || start.indexOf(state) >= 0 || (state % 2 && states === 1 && !start[0])) {
const pattern = rule.pattern;
pattern.lastIndex = lastIndex;
const result = pattern.exec(input);
const pattern = rule.pattern;
pattern.lastIndex = lastIndex;
const result = pattern.exec(input);
if (!result || result.index !== lastIndex) {
continue;
}
if (!result || result.index !== lastIndex) continue;
let j = matches.push({
result: result,
action: rule.action,
length: result[0].length,
});
let j = matches.push({
result,
action: rule.action,
length: result[0].length,
global: rule.global,
});
if (rule.global) {
index = j;
}
while (--j > 0) {
const k = j - 1;
const cur = matches[j];
const prev = matches[k];
const longer = cur.length > prev.length;
const tieFavorsCur = cur.length === prev.length && prev.global && !cur.global;
while (--j > index) {
const k = j - 1;
if (!longer && !tieFavorsCur) break;
matches[j] = prev;
matches[k] = cur;
if (matches[j].length > matches[k].length) {
const temple = matches[j];
matches[j] = matches[k];
matches[k] = temple;
}
}
}
}
return matches;
}
};
}
export default Lexer;

View File

@@ -20,7 +20,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.15.33",
"@swc/core": "^1.15.32",
"@webcomponents/template": "^1.5.1",
"base64-js": "^1.5.1",
"core-js": "^3.49.0",

View File

@@ -8,7 +8,6 @@ import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
import "#elements/utils/TimeDeltaHelp";
import "./AdminSettingsFooterLinks.js";
import "#elements/Alert";
import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.js";
@@ -288,9 +287,6 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
help=${msg(
"When enabled, other flow tabs in a session will refresh upon a successful authentication.",
)}
.bighelp=${html`<ak-alert class="pf-c-radio__description" inline plain>
${msg("This flag is deprecated.")}
</ak-alert>`}
>
</ak-switch-input>
<ak-switch-input

View File

@@ -52,6 +52,10 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
...AKModal.styles,
PFAbout,
css`
:host {
height: 100%;
}
.pf-c-about-modal-box {
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-dialog--BackgroundColor);
width: unset;

View File

@@ -135,7 +135,7 @@ export class AdminInterface extends WithCapabilitiesConfig(
WebsocketClient.connect();
this.#sidebarMatcher = window.matchMedia("(width > 1210px)");
this.#sidebarMatcher = window.matchMedia("(width >= 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches;
}

View File

@@ -197,17 +197,15 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
?checked=${this.instance?.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${this.instance?.metaHide ?? false}
label=${msg("Hide from My applications")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
)}
label=${msg("Hide from User Dashboard")}
help=${msg("Whether this application will be shown on the User Dashboard.")}
>
</ak-switch-input>
<ak-file-search-input

View File

@@ -18,7 +18,7 @@ import {
RACProvider,
RadiusProvider,
RedirectURI,
RedirectURITypeEnum,
RedirectUriTypeEnum,
SAMLProvider,
SCIMProvider,
WSFederationProvider,
@@ -87,7 +87,7 @@ function formatRedirectUris(uris: RedirectURI[] = []) {
(${uri.matchingMode === MatchingModeEnum.Strict
? msg("strict")
: msg("regexp")},
${uri.redirectUriType === RedirectURITypeEnum.Logout
${uri.redirectUriType === RedirectUriTypeEnum.Logout
? msg("post logout")
: msg("authorization")})
</li>`,

View File

@@ -183,16 +183,16 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
?checked=${app.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${app.metaHide ?? false}
label=${msg("Hide from My applications")}
label=${msg("Hide from User Dashboard")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
"Whether this application will be shown on the User Dashboard.",
)}
>
</ak-switch-input>

View File

@@ -124,22 +124,19 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
order="order"
.columns=${COLUMNS}
.content=${[]}
>
<ak-empty-state slot="empty-table" icon="pf-icon-module"
><span>${msg("No bound policies.")}</span>
<div slot="body">
${msg("No policies are currently bound to this object.")}
</div>
<div slot="primary">
<button
@click=${() => this.onBindingEvent()}
class="pf-c-button pf-m-primary"
>
${msg("Bind policy/group/user")}
</button>
</div>
</ak-empty-state>
</ak-select-table>
></ak-select-table>
<ak-empty-state icon="pf-icon-module"
><span>${msg("No bound policies.")}</span>
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
<div slot="primary">
<button
@click=${() => this.onBindingEvent()}
class="pf-c-button pf-m-primary"
>
${msg("Bind policy/group/user")}
</button>
</div>
</ak-empty-state>
</div>`;
}

View File

@@ -381,7 +381,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
return html`<h2 class="pf-c-wizard__main-title">
${msg("Review the Application and Provider")}
</h2>
<fieldset class="ak-c-fieldset" name="application-details">
<fieldset>
<legend>${msg("Application Details")}</legend>
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
@@ -419,7 +419,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
${
renderer
? html`<fieldset class="ak-c-fieldset" name="provider-details">
? html`<fieldset>
<legend>${msg("Provider Details")}</legend>
${renderer(provider)}
</fieldset>`

View File

@@ -23,7 +23,6 @@ import { certificateProvider, certificateSelector } from "#admin/brands/Certific
import {
Application,
AuthenticationEnum,
Brand,
CoreApi,
CoreApplicationsListRequest,
@@ -32,6 +31,7 @@ import {
FlowsApi,
UsageEnum,
} from "@goauthentik/api";
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum.js";
import YAML from "yaml";

View File

@@ -86,7 +86,7 @@ export class ConfigModal extends ModalButton {
></ak-codemirror>
</ak-expand>
</div>
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<fieldset class="pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
class="pf-c-button pf-m-plain"

View File

@@ -65,7 +65,7 @@ export class DeviceAddHowTo extends ModalButton {
})}
</ak-tabs>`}
</div>
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<fieldset class="pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
class="pf-c-button pf-m-primary"

View File

@@ -17,7 +17,6 @@ import { DesignationToLabel, LayoutToLabel } from "#admin/flows/utils";
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
import {
AuthenticationEnum,
DeniedActionEnum,
Flow,
FlowDesignationEnum,
@@ -25,6 +24,7 @@ import {
FlowsApi,
UsageEnum,
} from "@goauthentik/api";
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum.js";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";

View File

@@ -36,7 +36,7 @@ import {
OAuth2Provider,
OAuth2ProviderLogoutMethodEnum,
RedirectURI,
RedirectURITypeEnum,
RedirectUriTypeEnum,
SubModeEnum,
ValidationError,
} from "@goauthentik/api";
@@ -270,7 +270,7 @@ export function renderForm({
.newItem=${() => ({
matchingMode: MatchingModeEnum.Strict,
url: "",
redirectUriType: RedirectURITypeEnum.Authorization,
redirectUriType: RedirectUriTypeEnum.Authorization,
})}
.row=${(redirectURI: RedirectURI, idx: number) => {
return html`<ak-provider-oauth2-redirect-uri

View File

@@ -4,7 +4,7 @@ import { AKControlElement } from "#elements/ControlElement";
import { LitPropertyRecord } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { MatchingModeEnum, RedirectURI, RedirectURITypeEnum } from "@goauthentik/api";
import { MatchingModeEnum, RedirectURI, RedirectUriTypeEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, html } from "lit";
@@ -37,7 +37,7 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
public redirectURI: RedirectURI = {
matchingMode: MatchingModeEnum.Strict,
url: "",
redirectUriType: RedirectURITypeEnum.Authorization,
redirectUriType: RedirectUriTypeEnum.Authorization,
};
@property({ type: String, useDefault: true })
@@ -89,15 +89,15 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
@change=${onChange}
>
<option
value="${RedirectURITypeEnum.Authorization}"
value="${RedirectUriTypeEnum.Authorization}"
?selected=${(this.redirectURI.redirectUriType ??
RedirectURITypeEnum.Authorization) === RedirectURITypeEnum.Authorization}
RedirectUriTypeEnum.Authorization) === RedirectUriTypeEnum.Authorization}
>
${msg("Authorization")}
</option>
<option
value="${RedirectURITypeEnum.Logout}"
?selected=${this.redirectURI.redirectUriType === RedirectURITypeEnum.Logout}
value="${RedirectUriTypeEnum.Logout}"
?selected=${this.redirectURI.redirectUriType === RedirectUriTypeEnum.Logout}
>
${msg("Post Logout")}
</option>

View File

@@ -30,6 +30,7 @@ import {
GroupMatchingModeEnum,
OAuthSource,
OAuthSourceRequest,
PatchedOAuthSourceRequest,
PKCEMethodEnum,
ProviderTypeEnum,
SourcesApi,
@@ -81,6 +82,20 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
//#region Lifecycle
private get isAtProtocolSource(): boolean {
return (
this.providerType?.name === ProviderTypeEnum.Atproto ||
this.modelName?.includes("atproto") === true
);
}
private get isClientSecretRequired(): boolean {
if (this.isAtProtocolSource) {
return false;
}
return this.providerType?.clientSecretRequired !== false;
}
protected async loadInstance(pk: string): Promise<OAuthSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthRetrieve({
slug: pk,
@@ -97,16 +112,20 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
protected async send(data: OAuthSource): Promise<OAuthSource> {
data.providerType = (this.providerType?.name || "") as ProviderTypeEnum;
const requestData = data as unknown as OAuthSourceRequest & PatchedOAuthSourceRequest;
if (!this.isClientSecretRequired) {
requestData.consumerSecret = "";
}
if (this.instance) {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
slug: this.instance.slug,
patchedOAuthSourceRequest: data,
patchedOAuthSourceRequest: requestData,
});
}
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
oAuthSourceRequest: data as unknown as OAuthSourceRequest,
oAuthSourceRequest: requestData,
});
}
@@ -186,9 +205,11 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
autocomplete="off"
/>
<p class="pf-c-form__helper-text">
${msg(
"URL used to request the initial token. This URL is only required for OAuth 1.",
)}
${this.isAtProtocolSource
? msg("URL used to create pushed authorization requests.")
: msg(
"URL used to request the initial token. This URL is only required for OAuth 1.",
)}
</p>
</ak-form-element-horizontal> `
: nothing}
@@ -405,16 +426,22 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
spellcheck="false"
required
/>
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
<p class="pf-c-form__helper-text">
${this.isAtProtocolSource
? msg("Client metadata URL.")
: msg("Also known as Client ID.")}
</p>
</ak-form-element-horizontal>
<ak-secret-textarea-input
label=${msg("Consumer secret")}
name="consumerSecret"
input-hint="code"
help=${msg("Also known as Client Secret.")}
?required=${!this.instance}
?revealed=${!this.instance}
></ak-secret-textarea-input>
${this.isClientSecretRequired
? html`<ak-secret-textarea-input
label=${msg("Consumer secret")}
name="consumerSecret"
input-hint="code"
help=${msg("Also known as Client Secret.")}
?required=${!this.instance}
?revealed=${!this.instance}
></ak-secret-textarea-input>`
: nothing}
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
<input
type="text"

View File

@@ -37,6 +37,8 @@ export function ProviderToLabel(provider?: ProviderTypeEnum): string {
return "";
case ProviderTypeEnum.Apple:
return "Apple";
case ProviderTypeEnum.Atproto:
return "AT Protocol";
case ProviderTypeEnum.Azuread:
return "Azure Active Directory (Deprecated)";
case ProviderTypeEnum.Discord:

View File

@@ -1,6 +1,6 @@
import { ModelForm } from "#elements/forms/ModelForm";
import type { Stage } from "@goauthentik/api";
import type { Stage } from "@goauthentik/api/dist/models/Stage";
import { msg } from "@lit/localize";

View File

@@ -158,33 +158,24 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
<ak-radio
.options=${[
{
label: msg(
"No preference: the browser may offer any available authenticator",
),
label: msg("No preference is sent"),
value: null,
default: true,
},
{
label: msg(
"Platform: a non-removable authenticator built into the device, such as Touch ID, Face ID, or Windows Hello",
"A non-removable authenticator, like TouchID or Windows Hello",
),
value: AuthenticatorAttachmentEnum.Platform,
},
{
label: msg(
"Cross-platform: a roaming authenticator, such as a YubiKey or Google Titan",
),
label: msg('A "roaming" authenticator, like a YubiKey'),
value: AuthenticatorAttachmentEnum.CrossPlatform,
},
]}
.value=${this.instance?.authenticatorAttachment}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg(
"Controls the authenticatorAttachment parameter sent to the browser during WebAuthn registration. If Hints are configured and this is left as 'No preference', a value is inferred from the selected hints for backward compatibility with older browsers.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Hints")} name="hints">
<ak-dual-select-provider

View File

@@ -10,7 +10,7 @@ import { AKElement } from "#elements/Base";
import { Invitation, StagesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
@@ -27,30 +27,7 @@ export class InvitationListLink extends AKElement {
@property()
selectedFlow?: string;
/**
* When true, the "Send via Email" button dispatches the
* `ak-invitation-send-email-inline` event instead of opening the nested
* email modal. Used by the invitation wizard's success step so the email
* form can be rendered as its own wizard step.
*/
@property({ type: Boolean, attribute: "inline-send-email" })
inlineSendEmail = false;
static styles: CSSResult[] = [
PFForm,
PFFormControl,
PFDescriptionList,
PFButton,
css`
:host {
display: block;
width: 100%;
}
input.pf-c-form-control {
width: 100%;
}
`,
];
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList, PFButton];
renderLink(): string {
if (this.invitation?.flowObj) {
@@ -126,7 +103,6 @@ export class InvitationListLink extends AKElement {
class="pf-c-form-control"
readonly
type="text"
style="width: 100%;"
value=${this.renderLink()}
/>
</div>
@@ -146,32 +122,18 @@ export class InvitationListLink extends AKElement {
>
${msg("Copy Link")}
</button>
${this.inlineSendEmail
? html`<button
class="pf-c-button pf-m-secondary"
@click=${() => {
this.dispatchEvent(
new CustomEvent("ak-invitation-send-email-inline", {
bubbles: true,
composed: true,
}),
);
}}
>
${msg("Send via Email")}
</button>`
: html`<ak-forms-modal>
<span slot="submit">${msg("Send")}</span>
<span slot="header">${msg("Send Invitation via Email")}</span>
<ak-invitation-send-email-form
slot="form"
.invitation=${this.invitation}
>
</ak-invitation-send-email-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Send via Email")}
</button>
</ak-forms-modal>`}
<ak-forms-modal>
<span slot="submit">${msg("Send")}</span>
<span slot="header">${msg("Send Invitation via Email")}</span>
<ak-invitation-send-email-form
slot="form"
.invitation=${this.invitation}
>
</ak-invitation-send-email-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Send via Email")}
</button>
</ak-forms-modal>
</div>
</dd>
</div>

View File

@@ -1,8 +1,6 @@
import "#admin/rbac/ObjectPermissionModal";
import "#admin/stages/invitation/InvitationForm";
import "#admin/stages/invitation/InvitationListLink";
import "#admin/stages/invitation/wizard/InvitationWizard";
import "#elements/buttons/Dropdown";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import "#elements/forms/DeleteBulkForm";
@@ -11,7 +9,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, modalInvoker } from "#elements/dialogs";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
@@ -20,12 +18,11 @@ import { SlottedTemplateResult } from "#elements/types";
import { setPageDetails } from "#components/ak-page-navbar";
import { InvitationForm } from "#admin/stages/invitation/InvitationForm";
import { InvitationWizard } from "#admin/stages/invitation/wizard/InvitationWizard";
import { FlowDesignationEnum, Invitation, ModelEnum, StagesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, PropertyValues, TemplateResult } from "lit";
import { CSSResult, html, PropertyValues } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
@@ -142,66 +139,7 @@ export class InvitationListPage extends TablePage<Invitation> {
}
protected override renderObjectCreate(): SlottedTemplateResult {
return html`${this.renderNewInvitationDropdown()}`;
}
protected renderNewInvitationDropdown(): TemplateResult {
return html`<ak-dropdown class="pf-c-dropdown">
<div class="pf-c-dropdown__toggle pf-m-primary pf-m-split-button pf-m-action">
<button
class="pf-c-dropdown__toggle-button"
type="button"
${modalInvoker(InvitationWizard, { mode: "existing" })}
>
${msg("New Invitation")}
</button>
<button
class="pf-c-dropdown__toggle-button"
type="button"
id="new-invitation-toggle"
aria-haspopup="menu"
aria-controls="new-invitation-menu"
tabindex="0"
aria-label=${msg("New Invitation options")}
>
<i class="fas fa-caret-down" aria-hidden="true"></i>
</button>
</div>
<menu
class="pf-c-dropdown__menu"
hidden
id="new-invitation-menu"
aria-labelledby="new-invitation-toggle"
tabindex="-1"
>
<li role="presentation">
<button
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
${modalInvoker(InvitationWizard, { mode: "existing" })}
aria-description=${msg(
"Opens the new invitation wizard and binds the invitation to an existing enrollment flow.",
)}
>
${msg("with Existing Enrollment Flow...")}
</button>
</li>
<li role="presentation">
<button
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
${modalInvoker(InvitationWizard, { mode: "create" })}
aria-description=${msg(
"Opens the new invitation wizard, which will create a new enrollment flow and invitation stage.",
)}
>
${msg("with New Enrollment Flow and Invitation Stage...")}
</button>
</li>
</menu>
</ak-dropdown>`;
return ModalInvokerButton(InvitationForm);
}
protected override render(): SlottedTemplateResult {

View File

@@ -1,62 +0,0 @@
import "#admin/stages/invitation/wizard/InvitationWizardDetailsStep";
import "#admin/stages/invitation/wizard/InvitationWizardEmailStep";
import "#admin/stages/invitation/wizard/InvitationWizardFlowStep";
import "#admin/stages/invitation/wizard/InvitationWizardSuccessStep";
import "#elements/wizard/Wizard";
import { AKElement } from "#elements/Base";
import { TransclusionChildElement, TransclusionChildSymbol } from "#elements/dialogs";
import { SlottedTemplateResult } from "#elements/types";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { property } from "@lit/reactive-element/decorators/property.js";
import { html } from "lit";
export type InvitationWizardFlowMode = "existing" | "create";
@customElement("ak-invitation-wizard")
export class InvitationWizard extends AKElement implements TransclusionChildElement {
public static verboseName = msg("Invitation");
public [TransclusionChildSymbol] = true;
@property({ type: String })
public mode: InvitationWizardFlowMode = "existing";
protected override createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
protected override render(): SlottedTemplateResult {
return html`<ak-wizard
entity-singular=${msg("Invitation")}
description=${msg("Create a new invitation with an enrollment flow.")}
.initialSteps=${["flow-step", "details-step", "success-step"]}
>
<ak-invitation-wizard-flow-step
slot="flow-step"
headline=${msg("Enrollment Flow")}
.mode=${this.mode}
></ak-invitation-wizard-flow-step>
<ak-invitation-wizard-details-step
slot="details-step"
headline=${msg("Invitation Details")}
></ak-invitation-wizard-details-step>
<ak-invitation-wizard-success-step
slot="success-step"
headline=${msg("Invitation Link")}
></ak-invitation-wizard-success-step>
<ak-invitation-wizard-email-step
slot="email-step"
headline=${msg("Send via Email")}
></ak-invitation-wizard-email-step>
</ak-wizard>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard": InvitationWizard;
}
}

View File

@@ -1,261 +0,0 @@
import "#components/ak-switch-input";
import "#elements/CodeMirror";
import "#elements/forms/HorizontalFormElement";
import type { InvitationWizardState } from "./types";
import { DEFAULT_CONFIG } from "#common/api/config";
import {
parseAPIResponseError,
pluckErrorDetail,
pluckFallbackFieldErrors,
} from "#common/errors/network";
import { MessageLevel } from "#common/messages";
import { dateTimeLocal } from "#common/temporal";
import { showMessage } from "#elements/messages/MessageContainer";
import { WizardPage } from "#elements/wizard/WizardPage";
import { FlowsApi, ManagedApi, StagesApi } from "@goauthentik/api";
import YAML from "yaml";
import { msg, str } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
const MINIMAL_BLUEPRINT_PATH = "example/flows-invitation-enrollment-minimal.yaml";
@customElement("ak-invitation-wizard-details-step")
export class InvitationWizardDetailsStep extends WizardPage {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
@state()
invitationName = "";
@state()
invitationExpires: string = dateTimeLocal(new Date(Date.now() + 48 * 60 * 60 * 1000));
@state()
fixedDataRaw = "{}";
@state()
singleUse = true;
activeCallback = async (): Promise<void> => {
this.host.valid = this.invitationName.length > 0;
};
async #fail(step: string, err: unknown): Promise<false> {
const parsed = await parseAPIResponseError(err);
const fieldErrors = pluckFallbackFieldErrors(parsed);
const detail = fieldErrors.length > 0 ? fieldErrors.join(" ") : pluckErrorDetail(parsed);
showMessage({
level: MessageLevel.error,
message: msg(str`${step} failed`),
description: detail,
});
this.logger.error("Invitation wizard step failed", { step, error: err });
return false;
}
validate(): void {
let validYaml = true;
try {
YAML.parse(this.fixedDataRaw);
} catch {
validYaml = false;
}
this.host.valid =
this.invitationName.length > 0 && this.invitationExpires.length > 0 && validYaml;
}
nextCallback = async (): Promise<boolean> => {
if (!this.invitationName) return false;
let fixedData: Record<string, unknown> = {};
try {
fixedData = YAML.parse(this.fixedDataRaw) || {};
} catch {
return false;
}
const wizardState = this.host.state as unknown as InvitationWizardState;
if (wizardState.createdInvitationPk) {
return true;
}
wizardState.invitationName = this.invitationName;
wizardState.invitationExpires = this.invitationExpires;
wizardState.invitationFixedData = fixedData;
wizardState.invitationSingleUse = this.singleUse;
if (wizardState.needsFlow) {
try {
const result = await new ManagedApi(DEFAULT_CONFIG).managedBlueprintsImportCreate({
path: MINIMAL_BLUEPRINT_PATH,
context: JSON.stringify({
flow_name: wizardState.newFlowName,
flow_slug: wizardState.newFlowSlug,
stage_name: wizardState.newStageName,
continue_flow_without_invitation: wizardState.continueFlowWithoutInvitation,
user_type: wizardState.newUserType,
}),
});
if (!result.success) {
const logs = (result.logs || [])
.map((l) => l.event)
.filter((m) => !!m)
.join("\n");
return this.#fail(
msg("Importing enrollment flow blueprint"),
new Error(logs || msg("Blueprint validation failed")),
);
}
const slugToLookup = wizardState.newFlowSlug!;
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
slug: slugToLookup,
});
const createdFlow = flows.results[0];
if (!createdFlow) {
return this.#fail(
msg("Importing enrollment flow blueprint"),
new Error(
msg(str`Flow with slug "${slugToLookup}" not found after import`),
),
);
}
wizardState.createdFlowPk = createdFlow.pk;
wizardState.createdFlowSlug = createdFlow.slug;
wizardState.needsFlow = false;
wizardState.needsStage = false;
wizardState.needsBinding = false;
} catch (err) {
return this.#fail(msg("Importing enrollment flow blueprint"), err);
}
}
try {
const flowPk = wizardState.createdFlowPk || wizardState.selectedFlowPk || undefined;
const invitation = await new StagesApi(
DEFAULT_CONFIG,
).stagesInvitationInvitationsCreate({
invitationRequest: {
name: wizardState.invitationName!,
expires: wizardState.invitationExpires
? new Date(wizardState.invitationExpires)
: undefined,
fixedData: wizardState.invitationFixedData,
singleUse: wizardState.invitationSingleUse,
flow: flowPk || null,
},
});
wizardState.createdInvitationPk = invitation.pk;
wizardState.createdInvitation = invitation;
} catch (err) {
return this.#fail(msg("Creating invitation"), err);
}
return true;
};
override reset(): void {
this.invitationName = "";
this.invitationExpires = dateTimeLocal(new Date(Date.now() + 48 * 60 * 60 * 1000));
this.fixedDataRaw = "{}";
this.singleUse = true;
}
render(): TemplateResult {
const wizardState = this.host.state as unknown as InvitationWizardState;
const flowDisplay =
wizardState.flowMode === "existing"
? wizardState.selectedFlowSlug
: wizardState.newFlowSlug;
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Name")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.invitationName}
@input=${(ev: InputEvent) => {
const target = ev.target as HTMLInputElement;
this.invitationName = target.value.replace(/[^a-z0-9-]/g, "");
target.value = this.invitationName;
this.validate();
}}
/>
<p class="pf-c-form__helper-text">
${msg(
"The name of an invitation must be a slug: only lower case letters, numbers, and the hyphen are permitted here.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Expires")} required>
<input
type="datetime-local"
data-type="datetime-local"
class="pf-c-form-control"
required
.value=${this.invitationExpires}
@input=${(ev: InputEvent) => {
this.invitationExpires = (ev.target as HTMLInputElement).value;
this.validate();
}}
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Flow")}>
<input
type="text"
class="pf-c-form-control"
readonly
disabled
.value=${flowDisplay || ""}
/>
<p class="pf-c-form__helper-text">
${msg(
"The flow selected in the previous step. The invitation will be bound to this flow.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Custom attributes")}>
<ak-codemirror
mode="yaml"
.value=${this.fixedDataRaw}
@change=${(ev: CustomEvent) => {
this.fixedDataRaw = ev.detail.value;
this.validate();
}}
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg(
"Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.",
)}
</p>
</ak-form-element-horizontal>
<ak-switch-input
label=${msg("Single use")}
?checked=${this.singleUse}
@change=${(ev: Event) => {
this.singleUse = (ev.target as HTMLInputElement).checked;
}}
help=${msg("When enabled, the invitation will be deleted after usage.")}
></ak-switch-input>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-details-step": InvitationWizardDetailsStep;
}
}

View File

@@ -1,217 +0,0 @@
import "#components/ak-textarea-input";
import "#elements/forms/HorizontalFormElement";
import type { InvitationWizardState } from "./types";
import { DEFAULT_CONFIG } from "#common/api/config";
import {
parseAPIResponseError,
pluckErrorDetail,
pluckFallbackFieldErrors,
} from "#common/errors/network";
import { AKRefreshEvent } from "#common/events";
import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { SlottedTemplateResult } from "#elements/types";
import { WizardPage } from "#elements/wizard/WizardPage";
import { StagesApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-invitation-wizard-email-step")
export class InvitationWizardEmailStep extends WizardPage {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
@state()
toAddresses = "";
@state()
ccAddresses = "";
@state()
bccAddresses = "";
@state()
template = "email/invitation.html";
@state()
availableTemplates: TypeCreate[] = [];
override formatNextLabel(): SlottedTemplateResult {
return html`${msg("Send")}
<span class="pf-c-button__icon pf-m-end">
<i class="fas fa-paper-plane" aria-hidden="true"></i>
</span>`;
}
activeCallback = async (): Promise<void> => {
this.host.valid = this.toAddresses.trim().length > 0;
try {
this.availableTemplates = await new StagesApi(
DEFAULT_CONFIG,
).stagesEmailTemplatesList();
} catch {
this.availableTemplates = [];
}
};
parseEmailAddresses(raw: string): string[] {
return raw
.split(/[\n,;]/)
.map((value) => value.trim())
.filter((value) => value.length > 0);
}
validate(): void {
this.host.valid = this.parseEmailAddresses(this.toAddresses).length > 0;
}
nextCallback = async (): Promise<boolean> => {
const wizardState = this.host.state as unknown as InvitationWizardState;
const invitationPk = wizardState.createdInvitationPk;
if (!invitationPk) {
showMessage({
level: MessageLevel.error,
message: msg("No invitation available to send"),
});
return false;
}
const to = this.parseEmailAddresses(this.toAddresses);
if (to.length === 0) {
showMessage({
level: MessageLevel.error,
message: msg("Please enter at least one email address"),
});
return false;
}
const cc = this.parseEmailAddresses(this.ccAddresses);
const bcc = this.parseEmailAddresses(this.bccAddresses);
try {
await new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsSendEmailCreate({
inviteUuid: invitationPk,
invitationSendEmailRequest: {
emailAddresses: to,
ccAddresses: cc.length > 0 ? cc : undefined,
bccAddresses: bcc.length > 0 ? bcc : undefined,
template: this.template,
},
});
} catch (err) {
const parsed = await parseAPIResponseError(err);
const fieldErrors = pluckFallbackFieldErrors(parsed);
const detail =
fieldErrors.length > 0 ? fieldErrors.join(" ") : pluckErrorDetail(parsed);
showMessage({
level: MessageLevel.error,
message: msg("Failed to queue invitation emails"),
description: detail,
});
return false;
}
showMessage({
level: MessageLevel.success,
message: msg(
str`Invitation emails queued for sending to ${to.length} recipient(s). Check the System Tasks for more information.`,
),
});
this.dispatchEvent(new AKRefreshEvent());
return true;
};
override reset(): void {
this.toAddresses = "";
this.ccAddresses = "";
this.bccAddresses = "";
this.template = "email/invitation.html";
}
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("To")} required>
<textarea
class="pf-c-form-control"
required
rows="3"
.value=${this.toAddresses}
@input=${(ev: InputEvent) => {
this.toAddresses = (ev.target as HTMLTextAreaElement).value;
this.validate();
}}
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"One email address per line, or comma/semicolon separated. Each recipient will receive a separate email with an invitation link.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("CC")}>
<textarea
class="pf-c-form-control"
rows="2"
.value=${this.ccAddresses}
@input=${(ev: InputEvent) => {
this.ccAddresses = (ev.target as HTMLTextAreaElement).value;
}}
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"A comma-separated list of addresses to receive copies of the invitation. Recipients will receive the full list of other addresses in this list.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("BCC")}>
<textarea
class="pf-c-form-control"
rows="2"
.value=${this.bccAddresses}
@input=${(ev: InputEvent) => {
this.bccAddresses = (ev.target as HTMLTextAreaElement).value;
}}
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"A comma-separated list of addresses to receive copies of the invitation. Recipients will not receive the addresses of other recipients.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Template")} required>
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
this.template = (ev.target as HTMLSelectElement).value;
}}
>
${this.availableTemplates.map(
(template) =>
html`<option
value=${template.name}
?selected=${template.name === this.template}
>
${template.description}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${msg("Select the email template to use for sending invitations.")}
</p>
</ak-form-element-horizontal>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-email-step": InvitationWizardEmailStep;
}
}

View File

@@ -1,347 +0,0 @@
import "#components/ak-radio-input";
import "#components/ak-switch-input";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import type { InvitationWizardState } from "./types";
import { DEFAULT_CONFIG } from "#common/api/config";
import { WizardPage } from "#elements/wizard/WizardPage";
import {
FlowDesignationEnum,
type FlowSet,
type InvitationStage,
StagesApi,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
interface EnrollmentFlow {
slug: string;
pk: string;
name: string;
}
@customElement("ak-invitation-wizard-flow-step")
export class InvitationWizardFlowStep extends WizardPage {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl, PFButton, PFAlert];
@property({ type: String })
public mode: "existing" | "create" = "existing";
@state()
enrollmentFlows: EnrollmentFlow[] = [];
@state()
loading = true;
@state()
selectedFlowSlug?: string;
@state()
selectedFlowPk?: string;
@state()
newFlowName = "Enrollment with invitation";
@state()
newFlowSlug = "enrollment-with-invitation";
@state()
newStageName = "invitation-stage";
@state()
newUserType: "external" | "internal" = "external";
@state()
continueFlowWithoutInvitation = true;
activeCallback = async (): Promise<void> => {
this.host.valid = false;
if (this.mode === "create") {
this.loading = false;
this.validate();
return;
}
this.loading = true;
try {
const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({
noFlows: false,
});
const flowMap = new Map<string, EnrollmentFlow>();
stages.results.forEach((stage: InvitationStage) => {
(stage.flowSet || [])
.filter((flow: FlowSet) => flow.designation === FlowDesignationEnum.Enrollment)
.forEach((flow: FlowSet) => {
if (!flowMap.has(flow.slug)) {
flowMap.set(flow.slug, {
slug: flow.slug,
pk: flow.pk,
name: flow.name,
});
}
});
});
this.enrollmentFlows = Array.from(flowMap.values());
if (this.enrollmentFlows.length > 0) {
this.selectedFlowSlug = this.enrollmentFlows[0].slug;
this.selectedFlowPk = this.enrollmentFlows[0].pk;
this.host.valid = true;
}
} catch {
this.enrollmentFlows = [];
}
this.loading = false;
// If there's exactly one eligible flow, skip this step so the user goes
// straight to the invitation details. Drop ourselves from the step list
// so the back button from the next step doesn't bounce back here.
if (this.mode === "existing" && this.enrollmentFlows.length === 1) {
const currentSlot = this.slot;
const advanced = await this.host.navigateNext();
if (advanced) {
this.host.steps = this.host.steps.filter((s) => s !== currentSlot);
}
}
};
validate(): void {
if (this.mode === "existing") {
this.host.valid = !!this.selectedFlowSlug;
} else {
this.host.valid =
this.newFlowName.length > 0 &&
this.newFlowSlug.length > 0 &&
this.newStageName.length > 0;
}
}
nextCallback = async (): Promise<boolean> => {
const state = this.host.state as unknown as InvitationWizardState;
state.flowMode = this.mode;
if (this.mode === "existing") {
if (!this.selectedFlowSlug) return false;
state.selectedFlowSlug = this.selectedFlowSlug;
state.selectedFlowPk = this.selectedFlowPk;
state.needsFlow = false;
state.needsStage = false;
state.needsBinding = false;
} else {
if (!this.newFlowName || !this.newFlowSlug || !this.newStageName) return false;
state.newFlowName = this.newFlowName;
state.newFlowSlug = this.newFlowSlug;
state.newStageName = this.newStageName;
state.newUserType = this.newUserType;
state.continueFlowWithoutInvitation = this.continueFlowWithoutInvitation;
state.needsFlow = true;
state.needsStage = true;
state.needsBinding = true;
}
return true;
};
override reset(): void {
this.enrollmentFlows = [];
this.loading = true;
this.selectedFlowSlug = undefined;
this.selectedFlowPk = undefined;
this.newFlowName = "Enrollment with invitation";
this.newFlowSlug = "enrollment-with-invitation";
this.newStageName = "invitation-stage";
this.newUserType = "external";
this.continueFlowWithoutInvitation = true;
}
slugify(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
renderExistingFlowSelector(): TemplateResult {
if (this.enrollmentFlows.length === 0) {
return html`
<div class="pf-c-alert pf-m-warning pf-m-inline">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">
${msg("No enrollment flows with invitation stages found")}
</h4>
<div class="pf-c-alert__description">
<p>
${msg(
"You can create a new enrollment flow and invitation stage right here, or cancel and bind an invitation stage to an existing flow manually.",
)}
</p>
<button
type="button"
class="pf-c-button pf-m-primary"
@click=${() => {
this.mode = "create";
this.validate();
}}
>
${msg("Create a new enrollment flow")}
</button>
</div>
</div>
`;
}
return html`
<ak-form-element-horizontal label=${msg("Enrollment flow")} required>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<EnrollmentFlow[]> => {
if (!query) return this.enrollmentFlows;
const needle = query.toLowerCase();
return this.enrollmentFlows.filter(
(flow) =>
flow.name.toLowerCase().includes(needle) ||
flow.slug.toLowerCase().includes(needle),
);
}}
.renderElement=${(flow: EnrollmentFlow): string => flow.name}
.renderDescription=${(flow: EnrollmentFlow): TemplateResult =>
html`${flow.slug}`}
.value=${(flow: EnrollmentFlow | undefined): string | undefined => flow?.pk}
.selected=${(flow: EnrollmentFlow): boolean => flow.pk === this.selectedFlowPk}
@ak-change=${(ev: CustomEvent<{ value: EnrollmentFlow | null }>) => {
const flow = ev.detail.value;
this.selectedFlowSlug = flow?.slug;
this.selectedFlowPk = flow?.pk;
this.validate();
}}
style="display: block; width: 100%;"
></ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Only enrollment flows that have an invitation stage bound to them are listed here.",
)}
</p>
</ak-form-element-horizontal>
`;
}
renderCreateForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Flow name")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.newFlowName}
@input=${(ev: InputEvent) => {
const target = ev.target as HTMLInputElement;
this.newFlowName = target.value;
this.newFlowSlug = this.slugify(target.value);
this.validate();
}}
/>
<p class="pf-c-form__helper-text">${msg("Name for the new enrollment flow.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Flow slug")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.newFlowSlug}
@input=${(ev: InputEvent) => {
const target = ev.target as HTMLInputElement;
this.newFlowSlug = target.value.replace(/[^a-z0-9-]/g, "");
this.validate();
}}
/>
<p class="pf-c-form__helper-text">${msg("Visible in the URL.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Invitation stage name")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.newStageName}
@input=${(ev: InputEvent) => {
this.newStageName = (ev.target as HTMLInputElement).value;
this.validate();
}}
/>
<p class="pf-c-form__helper-text">${msg("Name for the new invitation stage.")}</p>
</ak-form-element-horizontal>
<ak-radio-input
label=${msg("User type")}
.value=${this.newUserType}
.options=${[
{
label: msg("External"),
value: "external",
description: html`${msg(
"Enrolled users are created as external (e.g. customers, guests). New users will be placed under users/external.",
)}`,
},
{
label: msg("Internal"),
value: "internal",
description: html`${msg(
"Enrolled users are created as internal (e.g. employees). New users will be placed under users/internal.",
)}`,
},
]}
@input=${(ev: CustomEvent<{ value: "external" | "internal" }>) => {
this.newUserType = ev.detail.value;
}}
></ak-radio-input>
<ak-switch-input
label=${msg("Continue flow without invitation")}
?checked=${this.continueFlowWithoutInvitation}
@change=${(ev: Event) => {
this.continueFlowWithoutInvitation = (ev.target as HTMLInputElement).checked;
}}
help=${msg(
"If enabled, the stage will jump to the next stage when no invitation is given. If disabled, the flow will be cancelled without a valid invitation.",
)}
></ak-switch-input>
`;
}
render(): TemplateResult {
if (this.loading) {
return html`<div class="pf-c-form">
<p>${msg("Loading...")}</p>
</div>`;
}
return html`<form class="pf-c-form pf-m-horizontal">
${this.mode === "existing"
? this.renderExistingFlowSelector()
: this.renderCreateForm()}
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-flow-step": InvitationWizardFlowStep;
}
}

View File

@@ -1,102 +0,0 @@
import "#admin/stages/invitation/InvitationListLink";
import type { InvitationWizardState } from "./types";
import { AKRefreshEvent } from "#common/events";
import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { WizardPage } from "#elements/wizard/WizardPage";
import { Invitation } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-invitation-wizard-success-step")
export class InvitationWizardSuccessStep extends WizardPage {
static styles: CSSResult[] = [
PFBase,
PFForm,
PFAlert,
css`
:host {
display: block;
width: 100%;
}
ak-stage-invitation-list-link {
display: block;
width: 100%;
}
`,
];
@state()
invitation?: Invitation;
#notified = false;
activeCallback = async (): Promise<void> => {
const wizardState = this.host.state as unknown as InvitationWizardState;
this.invitation = wizardState.createdInvitation;
this.host.valid = true;
if (this.invitation && !this.#notified) {
showMessage({
level: MessageLevel.success,
message: msg("Successfully created invitation."),
});
this.#notified = true;
}
};
nextCallback = async (): Promise<boolean> => {
this.dispatchEvent(new AKRefreshEvent());
return true;
};
override reset(): void {
this.invitation = undefined;
this.#notified = false;
}
render(): TemplateResult {
const invitation = this.invitation;
if (!invitation) {
return html`<div class="pf-c-alert pf-m-warning pf-m-inline">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">${msg("No invitation was created.")}</h4>
</div>`;
}
return html`
<ak-stage-invitation-list-link
.invitation=${invitation}
?inline-send-email=${true}
@ak-invitation-send-email-inline=${this.onSendViaEmail}
></ak-stage-invitation-list-link>
`;
}
onSendViaEmail = async (): Promise<void> => {
const steps = this.host.steps;
if (!steps.includes("email-step")) {
this.host.steps = [...steps, "email-step"];
}
await this.host.navigateNext();
};
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-success-step": InvitationWizardSuccessStep;
}
}

View File

@@ -1,31 +0,0 @@
import type { Invitation } from "@goauthentik/api";
export interface InvitationWizardState {
// Step 1: Flow selection
flowMode: "existing" | "create";
selectedFlowSlug?: string;
selectedFlowPk?: string;
newFlowName?: string;
newFlowSlug?: string;
newStageName?: string;
newUserType?: "external" | "internal";
continueFlowWithoutInvitation: boolean;
// Flags for which API calls to make
needsFlow: boolean;
needsStage: boolean;
needsBinding: boolean;
// Step 2: Invitation details
invitationName?: string;
invitationExpires?: string;
invitationFixedData?: Record<string, unknown>;
invitationSingleUse: boolean;
// Results from API calls
createdStagePk?: string;
createdFlowPk?: string;
createdFlowSlug?: string;
createdInvitationPk?: string;
createdInvitation?: Invitation;
}

View File

@@ -50,9 +50,7 @@ export class RedirectStageForm extends BaseStageForm<RedirectStage> {
protected override renderForm(): TemplateResult {
return html`<span>
${msg(
"Redirect the user to a static URL or another flow, optionally with all gathered context.",
)}
${msg("Redirect the user to another flow, potentially with all gathered context")}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input

View File

@@ -19,10 +19,10 @@ import {
CoreGroupsListRequest,
Group,
StagesApi,
UserCreationModeEnum,
UserTypeEnum,
UserWriteStage,
} from "@goauthentik/api";
import { UserCreationModeEnum } from "@goauthentik/api/dist/models/UserCreationModeEnum.js";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";

View File

@@ -159,7 +159,7 @@ export class UserBulkRevokeSessionsForm extends ModalButton {
>
</ak-user-bulk-revoke-sessions-table>
</section>
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<fieldset class="pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<ak-spinner-button
.callAction=${async () => {

View File

@@ -48,7 +48,7 @@
width: 100%;
}
@media (width <= 1210px) {
@media (width < 1200px) {
column-gap: calc(var(--pf-global--spacer--md) / 2);
}
}
@@ -137,7 +137,7 @@
display: none;
}
@media (width <= 1210px) {
@media (width < 1200px) {
display: none;
}
}
@@ -164,7 +164,7 @@
grid-area: toggle;
}
@media (width > 1210px) {
@media (width >= 1200px) {
slot[name="toggle"] {
display: none;
}

View File

@@ -22,12 +22,11 @@ function resolvePath(...args: string[]): string {
* - Intercepts local links and scrolls to the target element.
*/
export const MDXAnchor = ({
href: initialHref,
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const { publicDirectory } = useMDXModule();
let href = initialHref;
if (href?.startsWith(".") && publicDirectory) {
const nextPathname = resolvePath(publicDirectory, href);

View File

@@ -74,10 +74,6 @@ svg[id^="mermaid-svg-"] {
}
}
ak-alert + :is(h2, p) {
padding-top: var(--pf-global--spacer--md);
}
/* #region Dark Theme */
:host([theme="dark"]) {

View File

@@ -211,11 +211,9 @@ export class SimpleTable
);
return html`<tr role="presentation">
<td role="presentation" colspan=${columnCount + 1}>
<td role="presentation" colspan=${columnCount}>
<div class="pf-l-bullseye">
<slot name="empty-table">
<ak-empty-state><span>${message}</span></ak-empty-state>
</slot>
<ak-empty-state><span>${message}</span></ak-empty-state>
</div>
</td>
</tr>`;

View File

@@ -12,7 +12,7 @@
);
--ak-c-command-palette__group--Color: var(--pf-global--palette--purple-100);
--ak-c-fieldset--BorderColor: transparent;
--ak-fieldset--BorderColor: transparent;
--ak-c-command-palette__item--BackgroundColor: transparent;
--ak-c-command-palette__item--Color: var(--pf-global--palette--purple-50);
@@ -37,7 +37,7 @@
transform: translate3d(0, 0, 0); /* Fixes rendering artifacts. */
@media (prefers-contrast: more) {
--ak-c-fieldset--BorderColor: var(--pf-global--palette--purple-500);
--ak-fieldset--BorderColor: var(--pf-global--palette--purple-500);
}
}
@@ -109,7 +109,7 @@
transition-duration: 0.2s;
legend {
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
cursor: pointer;
color: var(--ak-c-command-palette__group--Color);

View File

@@ -524,7 +524,7 @@ export class AKCommandPaletteModal extends AKModal {
Object.entries(grouped),
(_group, groupIdx) => `group-${groupIdx}`,
([groupLabel, commands], groupIdx) => html`
<fieldset class="ak-c-fieldset" part="results-group">
<fieldset part="results-group">
<legend
class="${!groupLabel ? "sr-only more-contrast-only" : ""}"
data-label=${ifPresent(groupLabel)}

View File

@@ -115,8 +115,8 @@
/* #region Footer */
fieldset.ak-c-dialog__footer {
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
padding-block: calc(var(--ak-fieldset__legend--PaddingInlineBase) / 2);
border-inline: none;
border-block-end: none;

View File

@@ -3,7 +3,6 @@ import { checkObjectShallowEquality } from "#common/collections";
import { AKElement } from "#elements/Base";
import { asInvoker, type ModalTemplate } from "#elements/dialogs/invokers";
import type { DialogInit, TransclusionElementConstructor } from "#elements/dialogs/shared";
import { ElementConstructorBoundary } from "#elements/errors/boundaries";
import type { LitPropertyRecord } from "#elements/types";
import { isAKElementConstructor, StrictUnsafe } from "#elements/utils/unsafe";
@@ -160,22 +159,10 @@ export function lookupElementConstructor<T extends CustomElementConstructor>(
tagName: string,
registry: CustomElementRegistry = window.customElements,
): T {
if (!tagName) {
// eslint-disable-next-line no-console
console.trace(
"No tag name provided for lookup. Did this value come from a different version of authentik?",
);
return ElementConstructorBoundary as unknown as T;
}
const ElementConstructor = registry.get(tagName);
if (!ElementConstructor) {
// eslint-disable-next-line no-console
console.trace(`No custom element defined for tag name: ${tagName}`);
return ElementConstructorBoundary as unknown as T;
throw new TypeError(`No custom element defined for tag name: ${tagName}`);
}
return ElementConstructor as unknown as T;

View File

@@ -1,41 +0,0 @@
import { globalAK } from "#common/global";
import { AKElement } from "#elements/Base";
import { SlottedTemplateResult } from "#elements/types";
import { CapabilitiesEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit-html";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
/**
* A fallback element to render when a custom element fails to load, either due to a missing import,
* or a version mismatch between the element's definition and its usage.
*/
@customElement("ak-element-missing")
export class ElementConstructorBoundary extends AKElement {
public styles = [PFAlert];
protected override render(): SlottedTemplateResult {
const debug = globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug);
const description = debug
? msg(
"The element could not be loaded. This may be due to a missing import or a version mismatch.",
)
: msg(
"An element could not be loaded. Please try refreshing the page or clearing your cache.",
);
return html`<div class="pf-c-alert pf-m-danger" role="alert">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">${msg("Failed to load element")}</h4>
<div class="pf-c-alert__description">${description}</div>
</div>`;
}
}

View File

@@ -77,7 +77,7 @@ export class ConfirmationForm extends ModalButton {
<slot class="pf-c-content" name="body"></slot>
</form>
</section>
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<fieldset class="pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<ak-spinner-button
.callAction=${async () => {

View File

@@ -119,7 +119,7 @@ export class DeleteBulkForm<T> extends ModalButton {
>
</ak-used-by-table>
</section>
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<fieldset class="pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<ak-spinner-button
.callAction=${async () => {

View File

@@ -218,7 +218,7 @@ export class ModalForm extends ModalButton {
}
protected renderActions(): SlottedTemplateResult {
return html`<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
return html`<fieldset class="pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
type="button"

View File

@@ -20,14 +20,6 @@
}
}
/**
* P5 puts a line separating the entries, but this looks odd in our stacked usage. Specifying
* `.pf-m-stack` here also raises the specificity above the P4 default.
*/
.pf-m-stack label.pf-c-radio:not(:last-child) {
--pf-c-radio--BoxShadowColor: transparent;
}
.pf-c-radio__description {
text-wrap: balance;
text-wrap: pretty;

View File

@@ -996,9 +996,7 @@ export abstract class Table<T extends object, D = T>
* A simple pagination display, shown at both the top and bottom of the page.
*/
protected renderTablePagination(): SlottedTemplateResult {
if (!this.paginated || !this.data || this.data?.pagination.totalPages < 2) {
return nothing;
}
if (!this.paginated) return nothing;
const handler = (page: number) => {
this.page = page;

Some files were not shown because too many files have changed in this diff Show More