Compare commits
10 Commits
remote_deb
...
mdx-sans-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f7802a868 | ||
|
|
35f84aa61e | ||
|
|
d969619787 | ||
|
|
bafdd10f94 | ||
|
|
aaba353a9e | ||
|
|
abdff1c877 | ||
|
|
16fd8183b0 | ||
|
|
d3eaa3a4d9 | ||
|
|
02aba83017 | ||
|
|
e78c43e9d9 |
2
.github/actions/setup/action.yml
vendored
@@ -64,7 +64,7 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@cf525cb33f51aca27cd6fa02034117ab963ff9f1 # v2
|
||||
uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
|
||||
3
Makefile
@@ -118,9 +118,6 @@ run-worker: ## Run the main authentik worker process
|
||||
run-worker-watch: ## Run the authentik worker, with auto reloading
|
||||
watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs --no-meta --notify -- $(UV) run ak worker
|
||||
|
||||
debug-attach: ## Attach pdb to a running authentik Python worker (PEP 768). PID=<pid> to pick; SUDO=1 on macOS.
|
||||
$(UV) run python scripts/debug_attach.py
|
||||
|
||||
core-i18n-extract:
|
||||
$(UV) run ak makemessages \
|
||||
--add-location file \
|
||||
|
||||
@@ -1,31 +1,73 @@
|
||||
"""authentik API Modelviewset tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.admin.api.version_history import VersionHistoryViewSet
|
||||
from authentik.api.v3.urls import router
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.api.domains import DomainViewSet
|
||||
from authentik.tenants.api.tenants import TenantViewSet
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
|
||||
class TestModelViewSets(TestCase):
|
||||
"""Test Viewset"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet], full=True) -> dict[str, Callable]:
|
||||
"""Test Viewset"""
|
||||
|
||||
def tester(self: TestModelViewSets):
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
def test_attrs(self: TestModelViewSets) -> None:
|
||||
"""Test attributes we require on all viewsets"""
|
||||
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||
if not filterset_class:
|
||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||
|
||||
return tester
|
||||
def test_ordering(self: TestModelViewSets) -> None:
|
||||
"""Test that all ordering fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
for ordering_field in test_viewset.ordering:
|
||||
with self.subTest(ordering_field):
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'ordering': ordering_field}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_search(self: TestModelViewSets) -> None:
|
||||
"""Test that search fields are correct"""
|
||||
view = test_viewset.as_view({"get": "list"})
|
||||
req = self.factory.get(
|
||||
f"/?{urlencode({'search': generate_id()}, doseq=True)}", user=self.user
|
||||
)
|
||||
req.tenant = get_current_tenant()
|
||||
res = view(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
cases = {
|
||||
"attrs": test_attrs,
|
||||
}
|
||||
if full:
|
||||
cases["ordering"] = test_ordering
|
||||
cases["search"] = test_search
|
||||
return cases
|
||||
|
||||
|
||||
for _, viewset, _ in router.registry:
|
||||
if not issubclass(viewset, ModelViewSet | ReadOnlyModelViewSet):
|
||||
continue
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
||||
full = viewset not in [VersionHistoryViewSet, DomainViewSet, TenantViewSet]
|
||||
for test, case in viewset_tester_factory(viewset, full=full).items():
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}_{test}", case)
|
||||
|
||||
@@ -20,11 +20,16 @@ class TestBrands(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.default_flags = {}
|
||||
for flag in Flag.available(visibility="public"):
|
||||
self.default_flags[flag().key] = flag.get()
|
||||
Brand.objects.all().delete()
|
||||
|
||||
@property
|
||||
def default_flags(self) -> dict[str, object]:
|
||||
"""Get current public flags.
|
||||
|
||||
Some tests define temporary Flag subclasses, so this can't be cached in setUp.
|
||||
"""
|
||||
return {flag().key: flag.get() for flag in Flag.available(visibility="public")}
|
||||
|
||||
def test_current_brand(self):
|
||||
"""Test Current brand API"""
|
||||
brand = create_test_brand()
|
||||
|
||||
@@ -47,7 +47,8 @@ class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
||||
search_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"app",
|
||||
"app__name",
|
||||
"app__slug",
|
||||
"attributes",
|
||||
]
|
||||
filterset_fields = [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block head %}
|
||||
<style data-id="static-styles">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url|iriencode|safe }}");
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background-image: url("{{ flow_background_url }}");
|
||||
background-image: url("{{ flow_background_url|iriencode|safe }}");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||
<style data-id="flow-css">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ flow_background_url }}");
|
||||
--ak-global--background-image: url("{{ flow_background_url|iriencode|safe }}");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""stage view tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import RequestFactory as AuthentikRequestFactory
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
from authentik.flows.models import Flow, FlowStageBinding
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import FlowExecutorView
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
@@ -42,6 +44,46 @@ class TestViews(TestCase):
|
||||
"/static/dist/assets/images/flow_background.jpg",
|
||||
)
|
||||
|
||||
def test_flow_interface_css_background_preserves_presigned_url_query(self):
|
||||
"""Test flow CSS keeps signed URL query separators intact."""
|
||||
flow = create_test_flow()
|
||||
background_url = (
|
||||
"https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
|
||||
"?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
|
||||
"&X-Amz-Signature=signature"
|
||||
)
|
||||
|
||||
with patch.object(Flow, "background_url", return_value=background_url):
|
||||
response = self.client.get(
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
f'--ak-global--background-image: url("{background_url}");',
|
||||
html=False,
|
||||
)
|
||||
|
||||
def test_flow_sfe_css_background_preserves_presigned_url_query(self):
|
||||
"""Test SFE flow CSS keeps signed URL query separators intact."""
|
||||
flow = create_test_flow()
|
||||
background_url = (
|
||||
"https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
|
||||
"?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
|
||||
"&X-Amz-Signature=signature"
|
||||
)
|
||||
|
||||
with patch.object(Flow, "background_url", return_value=background_url):
|
||||
response = self.client.get(
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + "?sfe"
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
f'background-image: url("{background_url}");',
|
||||
html=False,
|
||||
)
|
||||
|
||||
|
||||
def view_tester_factory(view_class: type[StageView]) -> Callable:
|
||||
"""Test a form"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import faulthandler
|
||||
import os
|
||||
import random
|
||||
import signal
|
||||
@@ -77,12 +76,6 @@ def main(worker_id: int, socket_path: str):
|
||||
signal.signal(signal.SIGINT, immediate_shutdown)
|
||||
signal.signal(signal.SIGQUIT, immediate_shutdown)
|
||||
signal.signal(signal.SIGTERM, graceful_shutdown)
|
||||
# SIGUSR1 dumps every thread's traceback to stderr. Without this, the default
|
||||
# action is "terminate", which kills the worker (and trips the Rust supervisor).
|
||||
# Side-benefit: signal delivery wakes the eval loop, so `pdb -p` can attach to
|
||||
# an otherwise-idle worker parked in a C-level syscall.
|
||||
faulthandler.enable()
|
||||
faulthandler.register(signal.SIGUSR1)
|
||||
|
||||
random.seed()
|
||||
|
||||
@@ -104,11 +97,7 @@ def main(worker_id: int, socket_path: str):
|
||||
# Notify rust process that we are ready
|
||||
os.kill(os.getppid(), signal.SIGUSR2)
|
||||
|
||||
# Poll instead of waiting indefinitely so the main thread's eval loop ticks
|
||||
# periodically — PEP 768's debugger pending hook is serviced on the main
|
||||
# thread, and a permanent Event.wait() never returns to bytecode execution.
|
||||
while not shutdown.wait(timeout=1.0):
|
||||
pass
|
||||
shutdown.wait()
|
||||
|
||||
logger.info("Shutting down worker...")
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-29 00:28+0000\n"
|
||||
"POT-Creation-Date: 2026-04-30 00:27+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"
|
||||
@@ -224,6 +224,14 @@ msgid ""
|
||||
"providers are returned. When set to false, backchannel providers are excluded"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "Invalid password hash format. Must be a valid Django password hash."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "Cannot set both password and password_hash. Use only one."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/api/users.py
|
||||
msgid "No leading or trailing slashes allowed."
|
||||
msgstr ""
|
||||
|
||||
@@ -19,6 +19,7 @@ Forti
|
||||
Fortigate
|
||||
Gatus
|
||||
Gestionnaire
|
||||
ghec
|
||||
Gitea
|
||||
Gravitee
|
||||
Homarr
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
"""Attach pdb to a running authentik Python worker via PEP 768."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # nosec B404 — needed to launch ps and pdb
|
||||
import sys
|
||||
|
||||
PS_BIN = shutil.which("ps") or "/bin/ps"
|
||||
|
||||
|
||||
def list_python_procs() -> list[tuple[int, int, str]]:
|
||||
# argv is fully controlled, no shell, ps path resolved at startup.
|
||||
out = subprocess.check_output([PS_BIN, "-eo", "pid,ppid,command"], text=True) # nosec B603
|
||||
procs: list[tuple[int, int, str]] = []
|
||||
for line in out.splitlines()[1:]:
|
||||
try:
|
||||
pid_s, ppid_s, cmd = line.split(None, 2)
|
||||
except ValueError:
|
||||
continue
|
||||
if not pid_s.isdigit() or not ppid_s.isdigit():
|
||||
continue
|
||||
procs.append((int(pid_s), int(ppid_s), cmd))
|
||||
return procs
|
||||
|
||||
|
||||
def find_targets() -> list[tuple[int, str]]:
|
||||
# Match any authentik Python process: dev_server / runserver via manage.py,
|
||||
# gunicorn, dramatiq, or the worker_process supervisor. Go/Rust supervisors
|
||||
# and the `uv run` / shell wrappers don't match these patterns.
|
||||
needles = (
|
||||
"manage.py",
|
||||
"manage dev_server",
|
||||
"manage runserver",
|
||||
"gunicorn",
|
||||
"dramatiq",
|
||||
"lifecycle.worker_process",
|
||||
"lifecycle/worker_process",
|
||||
)
|
||||
matches = [(p, pp, c) for p, pp, c in list_python_procs() if any(n in c for n in needles)]
|
||||
matched_pids = {p for p, _, _ in matches}
|
||||
parents_of_matches = {pp for _, pp, _ in matches if pp in matched_pids}
|
||||
# A leaf is a match that isn't itself the parent of another match — this
|
||||
# picks the dev_server reloader child or the gunicorn worker, and still
|
||||
# includes single-process workers (which trivially have no child match).
|
||||
leaves = [(p, c) for p, _, c in matches if p not in parents_of_matches]
|
||||
return leaves
|
||||
|
||||
|
||||
def attach(pid: int) -> int:
|
||||
use_sudo = os.environ.get("SUDO") == "1"
|
||||
cmd = [sys.executable, "-m", "pdb", "-p", str(pid)]
|
||||
if use_sudo:
|
||||
cmd = ["sudo", "-E", *cmd]
|
||||
print(f"attaching pdb to pid {pid} (Ctrl-D or `quit` to detach)", file=sys.stderr)
|
||||
# cmd is built from sys.executable plus a digits-only PID.
|
||||
rc = subprocess.call(cmd) # nosec B603
|
||||
if rc != 0 and not use_sudo and sys.platform == "darwin":
|
||||
print(
|
||||
"\nattach failed. On macOS task_for_pid is restricted; "
|
||||
f"retry with: SUDO=1 make debug-attach PID={pid}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return rc
|
||||
|
||||
|
||||
def main() -> int:
|
||||
env_pid = os.environ.get("PID")
|
||||
if env_pid:
|
||||
if not env_pid.isdigit():
|
||||
print(f"PID={env_pid!r} is not numeric", file=sys.stderr)
|
||||
return 2
|
||||
return attach(int(env_pid))
|
||||
|
||||
targets = find_targets()
|
||||
if not targets:
|
||||
print(
|
||||
"no gunicorn/dramatiq Python workers found — is `make run-server` "
|
||||
"or `make run-worker` running?",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if len(targets) > 1:
|
||||
print("multiple worker candidates — pick one with PID=<pid>:", file=sys.stderr)
|
||||
for pid, cmd in targets:
|
||||
print(f" {pid}\t{cmd[:120]}", file=sys.stderr)
|
||||
return 1
|
||||
return attach(targets[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -83,7 +83,8 @@ pub(crate) fn start(tasks: &mut Tasks) -> Result<Arc<Metrics>> {
|
||||
"metrics",
|
||||
router.clone(),
|
||||
addr,
|
||||
true, // Allow failure in case the server is running on the same machine, like in dev
|
||||
config::get().debug, /* Allow failure in case the server is running on the same
|
||||
* machine, like in dev */
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -92,7 +93,8 @@ pub(crate) fn start(tasks: &mut Tasks) -> Result<Arc<Metrics>> {
|
||||
"metrics",
|
||||
router,
|
||||
unix::net::SocketAddr::from_pathname(socket_path())?,
|
||||
true, // Allow failure in case the server is running on the same machine, like in dev
|
||||
config::get().debug, /* Allow failure in case the server is running on the same machine,
|
||||
* like in dev */
|
||||
)?;
|
||||
|
||||
Ok(metrics)
|
||||
|
||||
@@ -328,8 +328,8 @@ pub(crate) fn start(_cli: Cli, tasks: &mut Tasks) -> Result<Arc<Workers>> {
|
||||
"worker",
|
||||
router.clone(),
|
||||
addr,
|
||||
true, /* Allow failure in case the server is running on the same machine, like
|
||||
* in dev. */
|
||||
config::get().debug, /* Allow failure in case the server is running on the same
|
||||
* machine, like in dev. */
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -338,7 +338,8 @@ pub(crate) fn start(_cli: Cli, tasks: &mut Tasks) -> Result<Arc<Workers>> {
|
||||
"worker",
|
||||
router,
|
||||
unix::net::SocketAddr::from_pathname(socket_path())?,
|
||||
true, // Allow failure in case the server is running on the same machine, like in dev.
|
||||
config::get().debug, /* Allow failure in case the server is running on the same
|
||||
* machine, like in dev. */
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
134
web/bundler/mdx-plugin/compile.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file Build-time markdown → HTML pipeline.
|
||||
*
|
||||
* The output is wrapped in a `<div class="pf-c-content" part="content">`
|
||||
* envelope so consuming `<ak-mdx>` elements can rely on PatternFly content
|
||||
* styles and expose CSS parts (`title`, `content`) to host pages.
|
||||
*/
|
||||
|
||||
import { rehypeAnchors, rehypeMermaid } from "./rehype.js";
|
||||
import {
|
||||
normalizeAdmonitionLabels,
|
||||
remarkAdmonition,
|
||||
remarkHeadings,
|
||||
remarkLists,
|
||||
} from "./remark.js";
|
||||
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
import apacheGrammar from "highlight.js/lib/languages/apache";
|
||||
import diffGrammar from "highlight.js/lib/languages/diff";
|
||||
import confGrammar from "highlight.js/lib/languages/ini";
|
||||
import nginxGrammar from "highlight.js/lib/languages/nginx";
|
||||
import { common } from "lowlight";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { unified } from "unified";
|
||||
import { parse as parseYAML } from "yaml";
|
||||
|
||||
/**
|
||||
* Pull a YAML frontmatter block off the top of `source` and return both
|
||||
* pieces. Returns an empty object if there is no frontmatter.
|
||||
*
|
||||
* @param {string} source
|
||||
* @returns {{ body: string, frontmatter: Record<string, unknown> }}
|
||||
*/
|
||||
function splitFrontmatter(source) {
|
||||
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
||||
if (!match) return { body: source, frontmatter: {} };
|
||||
const frontmatter = parseYAML(match[1]) || {};
|
||||
return { body: source.slice(match[0].length), frontmatter };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the wrapping `<div class="pf-c-content" part="content">` envelope
|
||||
* with an optional `<h1 part="title">` prefix, then serialize the whole
|
||||
* tree through `hast-util-to-html`. One serializer means one set of
|
||||
* escaping rules — no hand-rolled `&`/`<`/`>`/`"` replacement that has
|
||||
* to be remembered and audited separately.
|
||||
*
|
||||
* @param {import('hast').Element[]} bodyChildren Hast nodes from the markdown pipeline.
|
||||
* @param {string | null} title Frontmatter title, or `null` to omit the `<h1>`.
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderEnvelope(bodyChildren, title) {
|
||||
/** @type {import('hast').Element[]} */
|
||||
const children = [];
|
||||
|
||||
if (title) {
|
||||
children.push({
|
||||
type: "element",
|
||||
tagName: "h1",
|
||||
properties: { part: "title" },
|
||||
children: [{ type: "text", value: title }],
|
||||
});
|
||||
}
|
||||
|
||||
children.push(...bodyChildren);
|
||||
|
||||
/** @type {import('hast').Root} */
|
||||
const root = {
|
||||
type: "root",
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "div",
|
||||
properties: { className: ["pf-c-content"], part: "content" },
|
||||
children,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return toHtml(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a markdown source string to a wrapped HTML string and parsed
|
||||
* frontmatter. Used by the build-time plugin; the runtime side mirrors
|
||||
* this pipeline in the browser for admin-supplied prose.
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {string} publicDirectory Path of the file's directory inside the
|
||||
* docs site, used to resolve relative `<a>` hrefs at build time.
|
||||
* @returns {Promise<{ html: string, frontmatter: Record<string, unknown> }>}
|
||||
*/
|
||||
export async function compileMarkdown(source, publicDirectory) {
|
||||
const { body: rawBody, frontmatter } = splitFrontmatter(source);
|
||||
const body = normalizeAdmonitionLabels(rawBody);
|
||||
|
||||
// Run the pipeline up to (but not including) HTML stringification —
|
||||
// we want the hast tree so we can splice it into the envelope and
|
||||
// serialize the whole thing in one pass below.
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGFM)
|
||||
.use(remarkFrontmatter, ["yaml"])
|
||||
.use(remarkDirective)
|
||||
.use(remarkAdmonition)
|
||||
.use(remarkHeadings)
|
||||
.use(remarkLists)
|
||||
.use(remarkRehype, { allowDangerousHtml: false })
|
||||
.use(rehypeAnchors, { publicDirectory })
|
||||
.use(rehypeHighlight, {
|
||||
languages: {
|
||||
...common,
|
||||
nginx: nginxGrammar,
|
||||
apache: apacheGrammar,
|
||||
conf: confGrammar,
|
||||
diff: diffGrammar,
|
||||
},
|
||||
})
|
||||
.use(rehypeMermaid);
|
||||
|
||||
const tree = /** @type {import('hast').Root} */ (
|
||||
await processor.run(processor.parse(body), body)
|
||||
);
|
||||
|
||||
const title = typeof frontmatter.title === "string" ? frontmatter.title : null;
|
||||
const html = renderEnvelope(/** @type {import('hast').Element[]} */ (tree.children), title);
|
||||
|
||||
return { html, frontmatter };
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
/**
|
||||
* @file MDX plugin for ESBuild.
|
||||
* @file Markdown plugin for ESBuild.
|
||||
*
|
||||
* Resolves `~docs/...` imports to the website docs tree, then compiles each
|
||||
* `.md` / `.mdx` file to HTML at build time. The compiled HTML uses
|
||||
* `<ak-md-a>` and `<ak-alert>` custom elements so the runtime side can
|
||||
* stamp the HTML directly into shadow DOM without any client-side
|
||||
* JavaScript evaluation — this is what lets the page CSP drop
|
||||
* `'unsafe-eval'`.
|
||||
*
|
||||
* The on-load result is shipped via the `file` loader so the JSON travels
|
||||
* over the existing fetch-then-set-innerHTML path used by `<ak-mdx>`. The
|
||||
* shape is `{ content, frontmatter, publicPath, publicDirectory }` where
|
||||
* `content` is now pre-rendered HTML rather than raw markdown source.
|
||||
*
|
||||
* @import {
|
||||
* OnLoadArgs,
|
||||
@@ -14,35 +26,25 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||
import { compileMarkdown } from "./compile.js";
|
||||
|
||||
/**
|
||||
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData Data passed to `onload`.
|
||||
*
|
||||
* @typedef LoadDataFields Extra fields given in `data` to `onload`.
|
||||
* @property {PluginData | null | undefined} [pluginData] Plugin data.
|
||||
*
|
||||
* @typedef PluginData Extra data passed.
|
||||
* @property {Buffer | string | null | undefined} [contents] File contents.
|
||||
*/
|
||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||
|
||||
const pluginName = "mdx-plugin";
|
||||
|
||||
/**
|
||||
* @typedef MDXPluginOptions
|
||||
*
|
||||
* @property {string} root Root directory.
|
||||
* @property {string} root Repository root.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bundle MDX into JSON modules.
|
||||
* Bundle markdown and MDX source into JSON modules.
|
||||
*
|
||||
* @param {MDXPluginOptions} options
|
||||
* @returns {Plugin}
|
||||
*/
|
||||
export function mdxPlugin({ root }) {
|
||||
const prefix = "~docs";
|
||||
|
||||
// TODO: Replace with `resolvePackage` after NPM Workspaces support is added.
|
||||
const docsPackageRoot = path.resolve(MonoRepoRoot, "website");
|
||||
|
||||
@@ -59,32 +61,32 @@ export function mdxPlugin({ root }) {
|
||||
|
||||
return {
|
||||
path: path.join(docsPackageRoot, "docs", args.path.slice(prefix.length)),
|
||||
|
||||
pluginName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LoadData} data
|
||||
* @param {OnLoadArgs} args
|
||||
* @returns {Promise<OnLoadResult>}
|
||||
*/
|
||||
async function loadListener(data) {
|
||||
const content = String(
|
||||
data.pluginData &&
|
||||
data.pluginData.contents !== null &&
|
||||
data.pluginData.contents !== undefined
|
||||
? data.pluginData.contents
|
||||
: await fs.readFile(data.path),
|
||||
);
|
||||
async function loadListener(args) {
|
||||
const source = String(await fs.readFile(args.path));
|
||||
|
||||
const publicPath = path.resolve(
|
||||
"/",
|
||||
path.relative(path.join(root, "website", "docs"), data.path),
|
||||
path.relative(path.join(root, "website", "docs"), args.path),
|
||||
);
|
||||
const publicDirectory = path.dirname(publicPath);
|
||||
|
||||
const { html, frontmatter } = await compileMarkdown(source, publicDirectory);
|
||||
|
||||
return {
|
||||
contents: JSON.stringify({ content, publicPath, publicDirectory }),
|
||||
contents: JSON.stringify({
|
||||
content: html,
|
||||
frontmatter,
|
||||
publicPath,
|
||||
publicDirectory,
|
||||
}),
|
||||
loader: "file",
|
||||
pluginName,
|
||||
};
|
||||
|
||||
117
web/bundler/mdx-plugin/rehype.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file Rehype plugins for the build-time markdown pipeline.
|
||||
*/
|
||||
|
||||
import { CurrentReleaseDocsURL } from "@goauthentik/core/version/node";
|
||||
|
||||
import { SKIP, visit } from "unist-util-visit";
|
||||
|
||||
/**
|
||||
* Resolve a relative `href` against the docs base URL. Same logic the old
|
||||
* runtime `MDXAnchor` used: take a `./...` href relative to the file's
|
||||
* `publicDirectory`, drop trailing `index`/`.md`/`.mdx`, and absolutize
|
||||
* against {@linkcode CurrentReleaseDocsURL}.
|
||||
*
|
||||
* @param {string} href
|
||||
* @param {string} publicDirectory
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveDocsHref(href, publicDirectory) {
|
||||
// `new URL(...)` against `file:///` lets us reuse the browser-style
|
||||
// path resolver while preserving the hash and any query string.
|
||||
const joined = `${publicDirectory}/${href}`.replace(/\/{2,}/g, "/");
|
||||
const placeholder = new URL(joined, "file:///");
|
||||
const next = new URL(placeholder.pathname, CurrentReleaseDocsURL);
|
||||
next.pathname = next.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
next.search = placeholder.search;
|
||||
next.hash = placeholder.hash;
|
||||
return next.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehype plugin: resolve relative anchors at build time and wrap every
|
||||
* `<a>` in an `<ak-md-a>` light-DOM custom element. The wrapper attaches
|
||||
* the fragment-link click interceptor at runtime so clicks on
|
||||
* `<a href="#section">` scroll within the host shadow tree rather than
|
||||
* overwriting `location.hash` (which would yank the hash-routed SPA off
|
||||
* its current page).
|
||||
*
|
||||
* Wrapping (rather than replacing) keeps the real `<a>` element inside
|
||||
* `<ak-mdx>`'s shadow tree where the existing PatternFly link CSS in
|
||||
* `styles.css` applies. The wrapper itself uses `display: contents` so
|
||||
* it does not perturb inline-flow layout.
|
||||
*
|
||||
* @param {{ publicDirectory: string }} options
|
||||
*/
|
||||
export function rehypeAnchors({ publicDirectory }) {
|
||||
return (/** @type {import('hast').Root} */ tree) => {
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName !== "a") return;
|
||||
|
||||
const props = node.properties || (node.properties = {});
|
||||
const href = typeof props.href === "string" ? props.href : "";
|
||||
|
||||
if (!href) return;
|
||||
|
||||
if (href.startsWith(".")) {
|
||||
props.href = resolveDocsHref(href, publicDirectory);
|
||||
props.target = "_blank";
|
||||
props.rel = "noopener noreferrer";
|
||||
} else if (!href.startsWith("#")) {
|
||||
// Already-absolute external link: open in a new tab.
|
||||
props.target = "_blank";
|
||||
props.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
// Wrap the anchor in `<ak-md-a>` by mutating the node in
|
||||
// place: the `<a>`'s contents become a single child, the
|
||||
// outer node becomes the wrapper. Returning `SKIP` keeps
|
||||
// the visitor from descending into the freshly-stamped
|
||||
// child anchor (which would re-match this filter and
|
||||
// recurse forever).
|
||||
/** @type {import('hast').Element} */
|
||||
const original = {
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { ...props },
|
||||
children: node.children,
|
||||
};
|
||||
|
||||
node.tagName = "ak-md-a";
|
||||
node.properties = {};
|
||||
node.children = [original];
|
||||
|
||||
return SKIP;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehype plugin: replace `language-mermaid` code blocks with
|
||||
* `<ak-diagram>` elements carrying the mermaid source as text content.
|
||||
* `<ak-diagram>` reads its own `textContent` and renders the SVG, so no
|
||||
* wrapper element is needed.
|
||||
*/
|
||||
export function rehypeMermaid() {
|
||||
return (/** @type {import('hast').Root} */ tree) => {
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName !== "pre") return;
|
||||
const child = node.children?.[0];
|
||||
if (!child || child.type !== "element" || child.tagName !== "code") return;
|
||||
|
||||
const className = child.properties?.className ?? [];
|
||||
const classes = Array.isArray(className) ? className : [className];
|
||||
if (!classes.includes("language-mermaid")) return;
|
||||
|
||||
const source = (child.children ?? [])
|
||||
.map((c) => (c.type === "text" ? c.value : ""))
|
||||
.join("");
|
||||
|
||||
node.tagName = "ak-diagram";
|
||||
node.properties = {};
|
||||
node.children = [{ type: "text", value: source }];
|
||||
|
||||
return SKIP;
|
||||
});
|
||||
};
|
||||
}
|
||||
134
web/bundler/mdx-plugin/remark.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file Remark plugins for the build-time markdown pipeline.
|
||||
*
|
||||
* The runtime side (`src/elements/ak-mdx/remark/*`) mirrors a subset of
|
||||
* these. Keeping the shapes parallel makes it easier to spot drift when
|
||||
* either pipeline grows a new transform.
|
||||
*/
|
||||
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
const ADMONITIONS = new Set(["info", "warning", "danger", "note", "caution", "tip"]);
|
||||
|
||||
/**
|
||||
* `caution` and `tip` aren't first-class PatternFly alert levels — map
|
||||
* them onto the closest equivalent so PFAlert styles render correctly.
|
||||
*/
|
||||
const ADMONITION_LEVEL = {
|
||||
info: "pf-m-info",
|
||||
warning: "pf-m-warning",
|
||||
danger: "pf-m-danger",
|
||||
note: "pf-m-info",
|
||||
caution: "pf-m-warning",
|
||||
tip: "pf-m-success",
|
||||
};
|
||||
|
||||
/**
|
||||
* Match a Docusaurus-style admonition opening line:
|
||||
*
|
||||
* :::caution Reserved application slugs
|
||||
*
|
||||
* `remark-directive` only understands the spec form `:::name[label]{attrs}`
|
||||
* — a bare-space label silently falls through as plain text. We rewrite
|
||||
* the source so the directive parser sees the bracketed form and the
|
||||
* label is preserved as the directive's first paragraph.
|
||||
*/
|
||||
const ADMONITION_BARE_LABEL_RE = new RegExp(
|
||||
`^(:::(?:${[...ADMONITIONS].join("|")}))[ \\t]+(.+?)[ \\t]*$`,
|
||||
"gm",
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeAdmonitionLabels(source) {
|
||||
return source.replace(ADMONITION_BARE_LABEL_RE, "$1[$2]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin: convert `:::info` / `:::warning` / `:::danger` / `:::note`
|
||||
* directives into `<ak-alert>` elements with a level attribute. The first
|
||||
* child paragraph carrying the `directiveLabel` flag (i.e. `:::info[Title]`
|
||||
* syntax) is promoted to a `<strong>` so the title renders as a heading-ish
|
||||
* element inside the slot.
|
||||
*/
|
||||
export function remarkAdmonition() {
|
||||
return (/** @type {import('mdast').Root} */ tree) => {
|
||||
visit(tree, (node) => {
|
||||
if (
|
||||
node.type !== "containerDirective" &&
|
||||
node.type !== "leafDirective" &&
|
||||
node.type !== "textDirective"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!ADMONITIONS.has(node.name)) return;
|
||||
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
const data = node.data || (node.data = {});
|
||||
data.hName = tagName;
|
||||
data.hProperties = {
|
||||
...(data.hProperties || {}),
|
||||
...(node.attributes || {}),
|
||||
level:
|
||||
/** @type {Record<string, string>} */ (ADMONITION_LEVEL)[node.name] ??
|
||||
`pf-m-${node.name}`,
|
||||
};
|
||||
|
||||
const children = /** @type {any[]} */ (node.children || []);
|
||||
const labelIndex = children.findIndex(
|
||||
(c) => c.type === "paragraph" && c.data?.directiveLabel,
|
||||
);
|
||||
if (labelIndex !== -1) {
|
||||
const label = children[labelIndex];
|
||||
children[labelIndex] = {
|
||||
type: "paragraph",
|
||||
children: [{ type: "strong", children: label.children }],
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin: kebab-case heading slugs into `id` attributes.
|
||||
*/
|
||||
export function remarkHeadings() {
|
||||
/**
|
||||
* @param {{ value?: string, children?: any[] }} n
|
||||
* @returns {string}
|
||||
*/
|
||||
const flatten = (n) => {
|
||||
if (n.value) return n.value;
|
||||
if (n.children) return n.children.map(flatten).join("");
|
||||
return "";
|
||||
};
|
||||
|
||||
return (/** @type {import('mdast').Root} */ tree) => {
|
||||
visit(tree, "heading", (node) => {
|
||||
const id = flatten(node)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const data = node.data || (node.data = {});
|
||||
data.hProperties = { ...(data.hProperties || {}), id };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin: tag lists with PatternFly's content class.
|
||||
*/
|
||||
export function remarkLists() {
|
||||
return (/** @type {import('mdast').Root} */ tree) => {
|
||||
visit(tree, "list", (node) => {
|
||||
const data = node.data || (node.data = {});
|
||||
data.hProperties = {
|
||||
...(data.hProperties || {}),
|
||||
className: "pf-c-list",
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
786
web/package-lock.json
generated
@@ -35,7 +35,6 @@
|
||||
"@lit/localize-tools": "^0.8.1",
|
||||
"@lit/reactive-element": "^2.1.2",
|
||||
"@lit/task": "^1.0.3",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mrmarble/djangoql-completion": "^0.8.3",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
@@ -77,6 +76,7 @@
|
||||
"fuse.js": "^7.3.0",
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"lex": "^2025.11.0",
|
||||
@@ -85,6 +85,7 @@
|
||||
"lit-element": "^4.2.2",
|
||||
"lit-html": "^3.3.2",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mermaid": "^11.14.0",
|
||||
"node-domexception": "^2025.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -95,16 +96,14 @@
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"pseudolocale": "^2.2.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-mermaid": "^3.0.0",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx-frontmatter": "^5.2.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"storybook": "^10.2.1",
|
||||
"style-mod": "^4.1.3",
|
||||
"trusted-types": "^2.0.0",
|
||||
@@ -113,6 +112,7 @@
|
||||
"type-fest": "^5.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.1",
|
||||
@@ -1762,43 +1762,6 @@
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mdx-js/mdx": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz",
|
||||
"integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdx": "^2.0.0",
|
||||
"acorn": "^8.0.0",
|
||||
"collapse-white-space": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"estree-util-scope": "^1.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.0.0",
|
||||
"markdown-extensions": "^2.0.0",
|
||||
"recma-build-jsx": "^1.0.0",
|
||||
"recma-jsx": "^1.0.0",
|
||||
"recma-stringify": "^1.0.0",
|
||||
"rehype-recma": "^1.0.0",
|
||||
"remark-mdx": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"source-map": "^0.7.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdx-js/react": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz",
|
||||
@@ -5200,15 +5163,6 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree-jsx": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
|
||||
"integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
@@ -6478,15 +6432,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/astring": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
|
||||
"integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"astring": "bin/astring"
|
||||
}
|
||||
},
|
||||
"node_modules/async-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||
@@ -7301,16 +7246,6 @@
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/collapse-white-space": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
|
||||
"integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -8635,38 +8570,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/esast-util-from-estree": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
|
||||
"integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-visit": "^2.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/esast-util-from-js": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz",
|
||||
"integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"acorn": "^8.0.0",
|
||||
"esast-util-from-estree": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
@@ -9108,100 +9011,6 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-attach-comments": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
|
||||
"integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-build-jsx": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz",
|
||||
"integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"estree-walker": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-is-identifier-name": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
|
||||
"integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-scope": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz",
|
||||
"integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-to-js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz",
|
||||
"integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"astring": "^1.8.0",
|
||||
"source-map": "^0.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-value-to-estree": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz",
|
||||
"integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-visit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz",
|
||||
"integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
@@ -10274,21 +10083,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-dom": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
||||
"integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hastscript": "^9.0.0",
|
||||
"web-namespaces": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-html": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
||||
@@ -10307,22 +10101,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-html-isomorphic": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
|
||||
"integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-from-dom": "^5.0.0",
|
||||
"hast-util-from-html": "^2.0.0",
|
||||
"unist-util-remove-position": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-parse5": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
||||
@@ -10369,34 +10147,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-estree": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz",
|
||||
"integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-attach-comments": "^3.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"style-to-js": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||
@@ -10420,33 +10170,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"style-to-js": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-text": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
||||
@@ -10688,12 +10411,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inspect-with-kind": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz",
|
||||
@@ -12256,18 +11973,6 @@
|
||||
"integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/markdown-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
|
||||
"integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
@@ -12512,83 +12217,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
|
||||
"integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-expression": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
||||
"integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-jsx": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
|
||||
"integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"parse-entities": "^4.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdxjs-esm": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
|
||||
"integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
@@ -12710,37 +12338,6 @@
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid-isomorphic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid-isomorphic/-/mermaid-isomorphic-3.1.0.tgz",
|
||||
"integrity": "sha512-mzrvfEVjnJIkJlEqxp3eMuR1wS0TeLCH1VK5E/T5yzWaBwI3JqjJuw70yUIThSCDJ5bRs6O3rgfp00oBAbvSeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"katex": "^0.16.0",
|
||||
"mermaid": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid-isomorphic/node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||
@@ -12966,108 +12563,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-expression": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
|
||||
"integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-mdx-expression": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-jsx": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz",
|
||||
"integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"micromark-factory-mdx-expression": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-md": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz",
|
||||
"integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdxjs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz",
|
||||
"integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.0.0",
|
||||
"acorn-jsx": "^5.0.0",
|
||||
"micromark-extension-mdx-expression": "^3.0.0",
|
||||
"micromark-extension-mdx-jsx": "^3.0.0",
|
||||
"micromark-extension-mdx-md": "^2.0.0",
|
||||
"micromark-extension-mdxjs-esm": "^3.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdxjs-esm": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz",
|
||||
"integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||
@@ -13111,33 +12606,6 @@
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-mdx-expression": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz",
|
||||
"integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-space": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
|
||||
@@ -13339,31 +12807,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-events-to-acorn": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz",
|
||||
"integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-visit": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-html-tag-name": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
|
||||
@@ -13572,15 +13015,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minim": {
|
||||
"version": "0.23.8",
|
||||
"resolved": "https://registry.npmjs.org/minim/-/minim-0.23.8.tgz",
|
||||
@@ -15343,73 +14777,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-build-jsx": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz",
|
||||
"integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-util-build-jsx": "^3.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-jsx": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz",
|
||||
"integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn-jsx": "^5.0.0",
|
||||
"estree-util-to-js": "^2.0.0",
|
||||
"recma-parse": "^1.0.0",
|
||||
"recma-stringify": "^1.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-parse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz",
|
||||
"integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"esast-util-from-js": "^2.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-stringify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz",
|
||||
"integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-util-to-js": "^2.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
@@ -15482,34 +14849,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-mermaid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-mermaid/-/rehype-mermaid-3.0.0.tgz",
|
||||
"integrity": "sha512-fxrD5E4Fa1WXUjmjNDvLOMT4XB1WaxcfycFIWiYU0yEMQhcTDElc9aDFnbDFRLxG1Cfo1I3mfD5kg4sjlWaB+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-from-html-isomorphic": "^2.0.0",
|
||||
"hast-util-to-text": "^4.0.0",
|
||||
"mermaid-isomorphic": "^3.0.0",
|
||||
"mini-svg-data-uri": "^1.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-parse": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
|
||||
@@ -15525,21 +14864,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-recma": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz",
|
||||
"integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-to-estree": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-stringify": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz",
|
||||
@@ -15605,37 +14929,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-mdx": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz",
|
||||
"integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-mdx": "^3.0.0",
|
||||
"micromark-extension-mdxjs": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-mdx-frontmatter": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-5.2.0.tgz",
|
||||
"integrity": "sha512-U/hjUYTkQqNjjMRYyilJgLXSPF65qbLPdoESOkXyrwz2tVyhAnm4GUKhfXqOOS9W34M3545xEMq+aMpHgVjEeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"estree-util-value-to-estree": "^3.0.0",
|
||||
"toml": "^3.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-mdx-define": "^1.0.0",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-parse": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||
@@ -17408,24 +16701,6 @@
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
|
||||
"integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"style-to-object": "1.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
||||
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inline-style-parser": "0.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||
@@ -17695,12 +16970,6 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
@@ -18138,24 +17407,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-mdx-define": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-mdx-define/-/unist-util-mdx-define-1.1.2.tgz",
|
||||
"integrity": "sha512-9ncH7i7TN5Xn7/tzX5bE3rXgz1X/u877gYVAUB3mLeTKYJmQHmqKTDBi6BTGXV7AeolBCI9ErcVsOt2qryoD0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"estree-util-scope": "^1.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
@@ -18169,33 +17420,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position-from-estree": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz",
|
||||
"integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-remove-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
||||
"integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
"@lit/localize-tools": "^0.8.1",
|
||||
"@lit/reactive-element": "^2.1.2",
|
||||
"@lit/task": "^1.0.3",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mrmarble/djangoql-completion": "^0.8.3",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
@@ -153,6 +152,7 @@
|
||||
"fuse.js": "^7.3.0",
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"lex": "^2025.11.0",
|
||||
@@ -161,6 +161,7 @@
|
||||
"lit-element": "^4.2.2",
|
||||
"lit-html": "^3.3.2",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mermaid": "^11.14.0",
|
||||
"node-domexception": "^2025.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -171,16 +172,14 @@
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"pseudolocale": "^2.2.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-mermaid": "^3.0.0",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx-frontmatter": "^5.2.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"storybook": "^10.2.1",
|
||||
"style-mod": "^4.1.3",
|
||||
"trusted-types": "^2.0.0",
|
||||
@@ -189,6 +188,7 @@
|
||||
"type-fest": "^5.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.1",
|
||||
|
||||
@@ -6,22 +6,29 @@ import { Diagram } from "#elements/Diagram";
|
||||
|
||||
import { FlowsApi } from "@goauthentik/api";
|
||||
|
||||
import { PropertyValues } from "lit-element/lit-element.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-flow-diagram")
|
||||
export class FlowDiagram extends Diagram {
|
||||
@property()
|
||||
flowSlug?: string;
|
||||
@property({ type: String, useDefault: true })
|
||||
flowSlug: string | null = null;
|
||||
|
||||
refreshHandler = (): void => {
|
||||
this.diagram = undefined;
|
||||
protected override updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("flowSlug")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected refresh = (): void => {
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesDiagramRetrieve({
|
||||
slug: this.flowSlug || "",
|
||||
})
|
||||
.then((data) => {
|
||||
this.diagram = data.diagram;
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,24 +9,28 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-source-oauth-diagram")
|
||||
export class OAuthSourceDiagram extends Diagram {
|
||||
@property({ attribute: false })
|
||||
source?: OAuthSource;
|
||||
@property({ attribute: false, useDefault: true })
|
||||
public source: OAuthSource | null = null;
|
||||
|
||||
refreshHandler = (): void => {
|
||||
protected override syncDiagramContent = (): void => {
|
||||
if (!this.source) return;
|
||||
const graph = ["graph LR"];
|
||||
graph.push(`source[${msg(str`OAuth Source ${this.source.name}`)}]`);
|
||||
graph.push(
|
||||
|
||||
const graph = [
|
||||
"graph LR",
|
||||
`source[${msg(str`OAuth Source ${this.source.name}`)}]`,
|
||||
`source --> flow_manager["${UserMatchingModeToLabel(
|
||||
this.source.userMatchingMode || UserMatchingModeEnum.Identifier,
|
||||
)}"]`,
|
||||
);
|
||||
];
|
||||
|
||||
if (this.source.enrollmentFlow) {
|
||||
graph.push("flow_manager --> flow_enroll[Enrollment flow]");
|
||||
}
|
||||
|
||||
if (this.source.authenticationFlow) {
|
||||
graph.push("flow_manager --> flow_auth[Authentication flow]");
|
||||
}
|
||||
|
||||
this.diagram = graph.join("\n");
|
||||
};
|
||||
}
|
||||
|
||||
49
web/src/common/mermaid.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DOM_PURIFY_STRICT } from "#common/purify";
|
||||
import { ResolvedUITheme } from "#common/theme";
|
||||
|
||||
import type { Mermaid, MermaidConfig } from "mermaid";
|
||||
|
||||
export const DefaultMermaidConfig: Readonly<MermaidConfig> = {
|
||||
logLevel: "fatal",
|
||||
startOnLoad: false,
|
||||
flowchart: {
|
||||
curve: "linear",
|
||||
},
|
||||
htmlLabels: false,
|
||||
securityLevel: "strict",
|
||||
dompurifyConfig: DOM_PURIFY_STRICT,
|
||||
};
|
||||
|
||||
let lastActiveTheme: ResolvedUITheme | null = null;
|
||||
let mermaid: Mermaid | null = null;
|
||||
|
||||
/**
|
||||
* Load the Mermaid library and initialize it with the appropriate theme based on the provided UI theme.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Mermaid is only loaded once and cached for subsequent calls. Note that
|
||||
* Mermaid is a singleton and does not support multiple instances with different configurations.
|
||||
*/
|
||||
export async function loadMermaid(uiTheme: ResolvedUITheme): Promise<Mermaid> {
|
||||
if (!mermaid) {
|
||||
const mermaidModule = await import("mermaid");
|
||||
mermaid = mermaidModule.default;
|
||||
}
|
||||
|
||||
if (uiTheme && uiTheme === lastActiveTheme) {
|
||||
return mermaid;
|
||||
}
|
||||
|
||||
const theme = uiTheme === "dark" ? "dark" : "default";
|
||||
|
||||
mermaid.initialize({
|
||||
...DefaultMermaidConfig,
|
||||
theme,
|
||||
darkMode: uiTheme === "dark",
|
||||
});
|
||||
|
||||
lastActiveTheme = uiTheme;
|
||||
|
||||
return mermaid;
|
||||
}
|
||||
@@ -50,6 +50,17 @@ export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitiz
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Trusted types policy for HTML produced by our own build-time markdown
|
||||
* pipeline. The HTML is generated from source we own (the `mdx-plugin`),
|
||||
* including custom elements like `<ak-md-a>` and `<ak-alert>` that
|
||||
* DOMPurify's default tag list would strip. Treat the input as already
|
||||
* trusted and pass it through unmodified.
|
||||
*/
|
||||
export const CompiledMarkdownTrustPolicy = trustedTypes.createPolicy("authentik-markdown", {
|
||||
createHTML: (trustedHTML: string) => trustedHTML,
|
||||
});
|
||||
|
||||
/**
|
||||
* Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by
|
||||
* a trusted source, such as the brand API.
|
||||
|
||||
@@ -261,26 +261,29 @@ declare global {
|
||||
* @param hint The color scheme hint to use.
|
||||
* @param doc The document to apply the theme to.
|
||||
*/
|
||||
export const applyDocumentTheme = ((currentUITheme = resolveUITheme(), doc = document): void => {
|
||||
export const applyDocumentTheme = ((
|
||||
currentUITheme = resolveUITheme(),
|
||||
ownerDocument = document,
|
||||
): void => {
|
||||
console.debug(`authentik/theme (document): want to switch to ${currentUITheme} theme`);
|
||||
|
||||
const { themeChoice } = doc.documentElement.dataset;
|
||||
const { themeChoice } = ownerDocument.documentElement.dataset;
|
||||
|
||||
if (themeChoice && themeChoice !== "auto") {
|
||||
console.debug(
|
||||
`authentik/theme (document): skipping theme application due to explicit choice (${themeChoice})`,
|
||||
);
|
||||
|
||||
doc.dispatchEvent(new ThemeChangeEvent(themeChoice));
|
||||
ownerDocument.dispatchEvent(new ThemeChangeEvent(themeChoice));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
doc.documentElement.dataset.theme = currentUITheme;
|
||||
ownerDocument.documentElement.dataset.theme = currentUITheme;
|
||||
|
||||
console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`);
|
||||
|
||||
doc.dispatchEvent(new ThemeChangeEvent(currentUITheme));
|
||||
ownerDocument.dispatchEvent(new ThemeChangeEvent(currentUITheme));
|
||||
}) satisfies UIThemeListener;
|
||||
|
||||
/**
|
||||
|
||||
41
web/src/elements/Diagram.css
Normal file
@@ -0,0 +1,41 @@
|
||||
:host {
|
||||
--ak-mermaid-message-text: var(--pf-c-content--Color);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
svg#diagram {
|
||||
.node {
|
||||
rect,
|
||||
circle,
|
||||
ellipse,
|
||||
polygon,
|
||||
path {
|
||||
fill: var(--pf-global--BackgroundColor--300);
|
||||
}
|
||||
}
|
||||
|
||||
.rect {
|
||||
fill: var(
|
||||
--ak-mermaid-box-background-color,
|
||||
var(--pf-global--BackgroundColor--light-300)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
stroke-width: 4;
|
||||
fill: var(--ak-mermaid-message-text) !important;
|
||||
paint-order: stroke;
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Dark Theme */
|
||||
|
||||
:host([theme="dark"]) {
|
||||
--ak-mermaid-message-text: var(--ak-dark-foreground);
|
||||
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter);
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -1,98 +1,82 @@
|
||||
import "#elements/EmptyState";
|
||||
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { DOM_PURIFY_STRICT } from "#common/purify";
|
||||
import { ThemeChangeEvent } from "#common/theme";
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
import { loadMermaid } from "#common/mermaid";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { listen } from "#elements/decorators/listen";
|
||||
import Styles from "#elements/Diagram.css";
|
||||
import { EmptyState } from "#elements/EmptyState";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import mermaid, { MermaidConfig } from "mermaid";
|
||||
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { CSSResult, PropertyValues } from "lit";
|
||||
import { guard } from "lit-html/directives/guard.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
@customElement("ak-diagram")
|
||||
export class Diagram extends AKElement {
|
||||
@property({ attribute: false })
|
||||
diagram?: string;
|
||||
static styles: CSSResult[] = [Styles];
|
||||
|
||||
refreshHandler = (): void => {
|
||||
#diagram = "";
|
||||
@property({ attribute: false, useDefault: true })
|
||||
public get diagram(): string {
|
||||
return this.#diagram || this.textContent.trim() || "";
|
||||
}
|
||||
|
||||
public set diagram(value: string) {
|
||||
const previous = this.#diagram;
|
||||
this.#diagram = value.trim();
|
||||
|
||||
this.requestUpdate("diagram", previous);
|
||||
}
|
||||
|
||||
@listen(AKRefreshEvent, {
|
||||
target: window,
|
||||
})
|
||||
protected syncDiagramContent = (): void => {
|
||||
if (!this.textContent) return;
|
||||
this.diagram = this.textContent;
|
||||
};
|
||||
|
||||
handlerBound = false;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
config: MermaidConfig;
|
||||
loadingPlaceholder: EmptyState;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.config = {
|
||||
// The type definition for this says number
|
||||
// but the example use strings
|
||||
// and numbers don't work
|
||||
logLevel: "fatal",
|
||||
startOnLoad: false,
|
||||
flowchart: {
|
||||
curve: "linear",
|
||||
},
|
||||
htmlLabels: false,
|
||||
securityLevel: "strict",
|
||||
dompurifyConfig: DOM_PURIFY_STRICT,
|
||||
};
|
||||
mermaid.initialize(this.config);
|
||||
this.loadingPlaceholder = new EmptyState();
|
||||
this.loadingPlaceholder.loading = true;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
if (this.handlerBound) return;
|
||||
window.addEventListener(EVENT_REFRESH, this.refreshHandler);
|
||||
this.addEventListener(ThemeChangeEvent.eventName, ((ev: CustomEvent<UiThemeEnum>) => {
|
||||
if (ev.detail === UiThemeEnum.Dark) {
|
||||
this.config.theme = "dark";
|
||||
} else {
|
||||
this.config.theme = "default";
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.syncDiagramContent();
|
||||
}
|
||||
|
||||
protected renderMermaid(): Promise<SlottedTemplateResult> {
|
||||
return loadMermaid(this.activeTheme).then((mermaid) => {
|
||||
if (!this.diagram) {
|
||||
return null;
|
||||
}
|
||||
mermaid.initialize(this.config);
|
||||
}) as EventListener);
|
||||
this.handlerBound = true;
|
||||
this.refreshHandler();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(EVENT_REFRESH, this.refreshHandler);
|
||||
}
|
||||
return mermaid.render("diagram", this.diagram).then((result) => {
|
||||
result.bindFunctions?.(this.renderRoot as HTMLElement);
|
||||
|
||||
render(): TemplateResult {
|
||||
this.querySelectorAll("*").forEach((el) => {
|
||||
try {
|
||||
el.remove();
|
||||
} catch {
|
||||
console.debug(`authentik/diagram: failed to remove element ${el}`);
|
||||
}
|
||||
return unsafeHTML(result.svg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const { diagram, loadingPlaceholder, activeTheme } = this;
|
||||
|
||||
return guard([diagram, activeTheme], () => {
|
||||
if (!diagram) {
|
||||
return loadingPlaceholder;
|
||||
}
|
||||
|
||||
return until(this.renderMermaid(), loadingPlaceholder);
|
||||
});
|
||||
if (!this.diagram) {
|
||||
return html`<ak-empty-state loading></ak-empty-state>`;
|
||||
}
|
||||
return html`${until(
|
||||
mermaid.render("graph", this.diagram).then((r) => {
|
||||
r.bindFunctions?.(this.shadowRoot as unknown as Element);
|
||||
return unsafeHTML(r.svg);
|
||||
}),
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export abstract class Interface extends AKElement {
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.commandPalette.modal.setCommands(
|
||||
createCommonCommands().map((command) => ({
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import type { MDXModule } from "~docs/types";
|
||||
|
||||
/**
|
||||
* Fetches an MDX module from a URL or ESBuild static asset.
|
||||
*/
|
||||
export function fetchMDXModule(url: string | URL): Promise<MDXModule> {
|
||||
return fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch content: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching content", error);
|
||||
return { content: "", publicPath: "", publicDirectory: "" };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A context for the current MDX module.
|
||||
*/
|
||||
export const MDXModuleContext = createContext<MDXModule>({
|
||||
content: "",
|
||||
});
|
||||
|
||||
MDXModuleContext.displayName = "MDXModuleContext";
|
||||
|
||||
/**
|
||||
* A hook to access the current MDX module.
|
||||
*/
|
||||
export function useMDXModule(): MDXModule {
|
||||
return useContext(MDXModuleContext);
|
||||
}
|
||||
146
web/src/elements/ak-mdx/ak-mdx.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import "#elements/Alert";
|
||||
import "#elements/Diagram";
|
||||
import "#elements/ak-mdx/components/ak-md-a";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
import { BrandedHTMLPolicy, CompiledMarkdownTrustPolicy, sanitizeHTML } from "#common/purify";
|
||||
|
||||
import { compileRuntimeMarkdown } from "#elements/ak-mdx/markdown";
|
||||
import Styles from "#elements/ak-mdx/styles.css";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { DistDirectoryName, StaticDirectoryName } from "#paths";
|
||||
import OneDark from "#styles/atom/one-dark.css";
|
||||
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
|
||||
/**
|
||||
* The JSON envelope our build-time `mdx-plugin` emits for every imported
|
||||
* `.md` / `.mdx` file: the `content` field is **pre-rendered HTML**, not
|
||||
* raw markdown source.
|
||||
*/
|
||||
interface MarkdownModule {
|
||||
content: string;
|
||||
frontmatter?: Record<string, unknown>;
|
||||
publicPath?: string;
|
||||
publicDirectory?: string;
|
||||
}
|
||||
|
||||
async function fetchMarkdownModule(url: string | URL): Promise<MarkdownModule> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`Failed to fetch markdown: ${response.statusText}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacer applied to the compiled HTML before it is stamped into the
|
||||
* shadow DOM. Used by callers who need to substitute `{placeholder}`-style
|
||||
* tokens (e.g. proxy-provider sample configs).
|
||||
*/
|
||||
export type Replacer = (input: string) => string;
|
||||
|
||||
/**
|
||||
* Renders markdown into shadow DOM with no client-side JavaScript
|
||||
* evaluation. Two modes:
|
||||
*
|
||||
* - `url`: resolves to a JSON envelope produced by the build-time
|
||||
* `mdx-plugin`. The envelope's `content` is already HTML.
|
||||
* - `content`: an admin-supplied markdown string. Compiled in-browser
|
||||
* through a pure `unified` / remark / rehype pipeline (no `eval`,
|
||||
* no `Function`), then sanitized via `BrandedHTMLPolicy`.
|
||||
*/
|
||||
@customElement("ak-mdx")
|
||||
export class AKMDX extends AKElement {
|
||||
@property({ type: String, reflect: true, useDefault: true })
|
||||
public url: string | null = null;
|
||||
|
||||
@property()
|
||||
public content?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public replacers: Replacer[] = [];
|
||||
|
||||
@state()
|
||||
protected compiledTemplate: SlottedTemplateResult = null;
|
||||
|
||||
static styles = [
|
||||
// ---
|
||||
PFList,
|
||||
PFTable,
|
||||
PFContent,
|
||||
OneDark,
|
||||
Styles,
|
||||
];
|
||||
|
||||
public override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.hydrate();
|
||||
}
|
||||
|
||||
#applyReplacers(html: string): string {
|
||||
return this.replacers.reduce((acc, replacer) => replacer(acc), html);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL mode: HTML comes from our build-time pipeline. It may contain
|
||||
* custom-element tags (`<ak-alert>`, `<ak-md-a>`) that DOMPurify's
|
||||
* default tag list would strip, so we route it through a Trusted
|
||||
* Types policy that passes the input through unmodified.
|
||||
*/
|
||||
async #hydrateFromURL(url: string): Promise<SlottedTemplateResult> {
|
||||
const { relBase } = globalAK().api;
|
||||
const pathname =
|
||||
relBase +
|
||||
StaticDirectoryName +
|
||||
"/" +
|
||||
DistDirectoryName +
|
||||
url.slice(url.indexOf("/assets"));
|
||||
const module = await fetchMarkdownModule(pathname);
|
||||
|
||||
if (module.publicDirectory) {
|
||||
this.dataset.publicDirectory = module.publicDirectory;
|
||||
}
|
||||
|
||||
const trustedHTML = CompiledMarkdownTrustPolicy.createHTML(
|
||||
this.#applyReplacers(module.content),
|
||||
);
|
||||
|
||||
return unsafeHTML(trustedHTML.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Content mode: admin-supplied markdown compiled in-browser through
|
||||
* a pure `unified` / remark / rehype pipeline (no `eval`, no
|
||||
* `Function`), then sanitized via `BrandedHTMLPolicy`.
|
||||
*/
|
||||
async #hydrateFromContent(source: string): Promise<SlottedTemplateResult> {
|
||||
const html = this.#applyReplacers(await compileRuntimeMarkdown(source));
|
||||
return sanitizeHTML(BrandedHTMLPolicy, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `url` or `content` into a template result and stash it on
|
||||
* reactive state. After this completes, Lit's render takes over.
|
||||
*/
|
||||
protected async hydrate(): Promise<void> {
|
||||
this.compiledTemplate = this.url
|
||||
? await this.#hydrateFromURL(this.url)
|
||||
: await this.#hydrateFromContent(this.content ?? "");
|
||||
}
|
||||
|
||||
public override render(): SlottedTemplateResult {
|
||||
return this.compiledTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-mdx": AKMDX;
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import "#elements/Alert";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
import { BrandedHTMLPolicy } from "#common/purify";
|
||||
|
||||
import { MDXAnchor } from "#elements/ak-mdx/components/MDXAnchor";
|
||||
import { MDXWrapper } from "#elements/ak-mdx/components/MDXWrapper";
|
||||
import { fetchMDXModule, MDXModuleContext } from "#elements/ak-mdx/MDXModuleContext";
|
||||
import { remarkAdmonition } from "#elements/ak-mdx/remark/remark-admonition";
|
||||
import { remarkHeadings } from "#elements/ak-mdx/remark/remark-headings";
|
||||
import { remarkLists } from "#elements/ak-mdx/remark/remark-lists";
|
||||
import Styles from "#elements/ak-mdx/styles.css";
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { DistDirectoryName, StaticDirectoryName } from "#paths";
|
||||
import OneDark from "#styles/atom/one-dark.css";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import { compile as compileMDX, run as runMDX } from "@mdx-js/mdx";
|
||||
import apacheGrammar from "highlight.js/lib/languages/apache";
|
||||
import diffGrammar from "highlight.js/lib/languages/diff";
|
||||
import confGrammar from "highlight.js/lib/languages/ini";
|
||||
import nginxGrammar from "highlight.js/lib/languages/nginx";
|
||||
import { common } from "lowlight";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import * as runtime from "react/jsx-runtime";
|
||||
import rehypeHighlight, { Options as HighlightOptions } from "rehype-highlight";
|
||||
import rehypeMermaid, { RehypeMermaidOptions } from "rehype-mermaid";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
||||
import remarkParse from "remark-parse";
|
||||
import type { MDXModule } from "~docs/types";
|
||||
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
|
||||
const highlightThemeOptions: HighlightOptions = {
|
||||
languages: {
|
||||
...common,
|
||||
nginx: nginxGrammar,
|
||||
apache: apacheGrammar,
|
||||
conf: confGrammar,
|
||||
diff: diffGrammar,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A replacer function that can be used to modify the output of the MDX component.
|
||||
*/
|
||||
export type Replacer = (input: string) => string;
|
||||
|
||||
@customElement("ak-mdx")
|
||||
export class AKMDX extends AKElement {
|
||||
// HACK: Fixes Lit Analyzer's parsing of TSX files with decorators.
|
||||
|
||||
@((property as typeof property)({ type: String, reflect: true }))
|
||||
public url?: string;
|
||||
|
||||
@((property as typeof property)())
|
||||
public content?: string;
|
||||
|
||||
@((property as typeof property)({ attribute: false }))
|
||||
public replacers: Replacer[] = [];
|
||||
|
||||
#reactRoot: Root | null = null;
|
||||
|
||||
static styles = [
|
||||
// ---
|
||||
|
||||
PFList,
|
||||
PFTable,
|
||||
PFContent,
|
||||
OneDark,
|
||||
Styles,
|
||||
];
|
||||
|
||||
public async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#reactRoot = createRoot(this.shadowRoot!);
|
||||
|
||||
let nextMDXModule: MDXModule | undefined;
|
||||
const { relBase } = globalAK().api;
|
||||
|
||||
if (this.url) {
|
||||
const pathname =
|
||||
relBase +
|
||||
StaticDirectoryName +
|
||||
"/" +
|
||||
DistDirectoryName +
|
||||
this.url.slice(this.url.indexOf("/assets"));
|
||||
|
||||
nextMDXModule = await fetchMDXModule(pathname);
|
||||
} else {
|
||||
nextMDXModule = {
|
||||
content: `${BrandedHTMLPolicy.createHTML(this.content || "")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return this.delegateRender(nextMDXModule);
|
||||
}
|
||||
|
||||
protected async delegateRender(mdxModule: MDXModule): Promise<void> {
|
||||
if (!this.#reactRoot) return;
|
||||
|
||||
const normalized = this.replacers.reduce(
|
||||
(content, replacer) => replacer(content),
|
||||
mdxModule.content,
|
||||
);
|
||||
|
||||
const mdx = await compileMDX(normalized, {
|
||||
outputFormat: "function-body",
|
||||
remarkPlugins: [
|
||||
remarkParse,
|
||||
remarkDirective,
|
||||
remarkAdmonition,
|
||||
remarkGFM,
|
||||
remarkFrontmatter,
|
||||
remarkMdxFrontmatter,
|
||||
remarkHeadings,
|
||||
remarkLists,
|
||||
],
|
||||
rehypePlugins: [
|
||||
// ---
|
||||
[rehypeHighlight, highlightThemeOptions],
|
||||
[
|
||||
rehypeMermaid,
|
||||
{
|
||||
prefix: "mermaid-svg-",
|
||||
colorScheme: this.activeTheme === UiThemeEnum.Dark ? "dark" : "light",
|
||||
} satisfies RehypeMermaidOptions,
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const { default: Content, ...mdxExports } = await runMDX(mdx, {
|
||||
...runtime,
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
|
||||
const { frontmatter = {} } = mdxExports;
|
||||
this.#reactRoot.render(
|
||||
<MDXModuleContext.Provider value={mdxModule}>
|
||||
<Content
|
||||
frontmatter={frontmatter}
|
||||
components={{
|
||||
wrapper: MDXWrapper,
|
||||
a: MDXAnchor,
|
||||
}}
|
||||
/>
|
||||
</MDXModuleContext.Provider>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-mdx": AKMDX;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useMDXModule } from "#elements/ak-mdx/MDXModuleContext";
|
||||
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* A simplified version of Node's `path.resolve`:
|
||||
*/
|
||||
function resolvePath(...args: string[]): string {
|
||||
const pathname = args
|
||||
// Combine all arguments into a single path...
|
||||
.join("/")
|
||||
// Normalizing any delimiting slashes...
|
||||
.replace(/\/{2,}/g, "/");
|
||||
|
||||
return new URL(pathname, "file:///").pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom anchor element that applies special behavior for MDX content.
|
||||
*
|
||||
* - Resolves relative links to the public directory in the public docs domain.
|
||||
* - Intercepts local links and scrolls to the target element.
|
||||
*/
|
||||
export const MDXAnchor = ({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const { publicDirectory } = useMDXModule();
|
||||
|
||||
if (href?.startsWith(".") && publicDirectory) {
|
||||
const nextPathname = resolvePath(publicDirectory, href);
|
||||
|
||||
const nextURL = new URL(nextPathname, import.meta.env.AK_DOCS_URL);
|
||||
// Remove trailing .md and .mdx, and trailing "index".
|
||||
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
href = nextURL.toString();
|
||||
}
|
||||
|
||||
const interceptHeadingLinks = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!href || !href.startsWith("#")) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rootNode = event.currentTarget.getRootNode() as ShadowRoot;
|
||||
|
||||
const elementID = href.slice(1);
|
||||
const target = rootNode.getElementById(elementID);
|
||||
|
||||
if (!target) {
|
||||
console.warn(`Element with ID ${elementID} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={interceptHeadingLinks}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export interface MDXWrapperProps {
|
||||
children: React.ReactNode;
|
||||
frontmatter: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for MDX content that adds a title if one is provided in the frontmatter.
|
||||
*/
|
||||
export const MDXWrapper = ({ children, frontmatter }: MDXWrapperProps) => {
|
||||
const { title } = frontmatter;
|
||||
const nextChildren = React.Children.toArray(children);
|
||||
|
||||
if (title) {
|
||||
nextChildren.unshift(
|
||||
<h1 key="header-title" part="title">
|
||||
{title}
|
||||
</h1>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pf-c-content" part="content">
|
||||
{nextChildren}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
63
web/src/elements/ak-mdx/components/ak-md-a.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { css, PropertyValues } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-md-a")
|
||||
export class AKMarkdownAnchor extends AKElement {
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected defaultSlot: HTMLSlotElement = this.ownerDocument.createElement("slot");
|
||||
|
||||
protected override render() {
|
||||
return this.defaultSlot;
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const anchors = this.defaultSlot
|
||||
.assignedElements({ flatten: true })
|
||||
.filter((element) => element.matches("a"));
|
||||
|
||||
for (const anchor of anchors) {
|
||||
anchor.addEventListener("click", this.clickListener);
|
||||
}
|
||||
}
|
||||
|
||||
protected clickListener(event: MouseEvent): void {
|
||||
const anchor = event.currentTarget as HTMLAnchorElement;
|
||||
const href = anchor.getAttribute("href");
|
||||
|
||||
if (!href || !href.startsWith("#")) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rootNode = anchor.getRootNode() as ShadowRoot;
|
||||
|
||||
const elementID = href.slice(1);
|
||||
const target = rootNode.getElementById(elementID);
|
||||
|
||||
if (!target) {
|
||||
console.warn(`Element with ID ${elementID} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-md-a": AKMarkdownAnchor;
|
||||
}
|
||||
}
|
||||
46
web/src/elements/ak-mdx/markdown.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
normalizeAdmonitionLabels,
|
||||
remarkAdmonition,
|
||||
} from "#elements/ak-mdx/remark/remark-admonition";
|
||||
import { remarkHeadings } from "#elements/ak-mdx/remark/remark-headings";
|
||||
import { remarkLists } from "#elements/ak-mdx/remark/remark-lists";
|
||||
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { unified } from "unified";
|
||||
|
||||
/**
|
||||
* Compile an admin-supplied markdown string to an HTML string in the
|
||||
* browser. The pipeline is a strict subset of the build-time one: no
|
||||
* syntax highlighting, no anchor rewriting — the output is plain HTML
|
||||
* that the existing `BrandedHTMLPolicy` (DOMPurify) sanitizes cleanly.
|
||||
*
|
||||
* Unlike `@mdx-js/mdx`'s `evaluate` / `run`, none of the `unified`,
|
||||
* `remark-*`, or `rehype-*` packages execute the input as JavaScript:
|
||||
* they are pure tree transformers. This is what lets us drop
|
||||
* `'unsafe-eval'` from the page CSP.
|
||||
*/
|
||||
export async function compileRuntimeMarkdown(source: string): Promise<string> {
|
||||
if (!source.trim()) return "";
|
||||
|
||||
// Translate Docusaurus's `:::name Title` syntax to `:::name[Title]`
|
||||
// before remark-directive parses it; otherwise it falls through as
|
||||
// plain text.
|
||||
const normalized = normalizeAdmonitionLabels(source);
|
||||
|
||||
const file = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGFM)
|
||||
.use(remarkDirective)
|
||||
.use(remarkAdmonition)
|
||||
.use(remarkHeadings)
|
||||
.use(remarkLists)
|
||||
.use(remarkRehype, { allowDangerousHtml: false })
|
||||
.use(rehypeStringify)
|
||||
.process(normalized);
|
||||
|
||||
return String(file);
|
||||
}
|
||||
@@ -1,35 +1,77 @@
|
||||
import { UnwrapSet } from "#common/sets";
|
||||
|
||||
import { h } from "hastscript";
|
||||
import type { Root } from "mdast";
|
||||
import type { Paragraph, Root } from "mdast";
|
||||
import type { Directives } from "mdast-util-directive";
|
||||
import type { Plugin } from "unified";
|
||||
import { visit } from "unist-util-visit";
|
||||
import type { VFile } from "vfile";
|
||||
|
||||
const ADMONITION_TYPES = new Set(["info", "warning", "danger", "note"]);
|
||||
export const ADMONITION_TYPES = new Set([
|
||||
"info",
|
||||
"warning",
|
||||
"danger",
|
||||
"note",
|
||||
"caution",
|
||||
"tip",
|
||||
] as const);
|
||||
|
||||
export type AdmonitionType = UnwrapSet<typeof ADMONITION_TYPES>;
|
||||
|
||||
export function isAdmonitionType(value: string): value is AdmonitionType {
|
||||
return ADMONITION_TYPES.has(value as AdmonitionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin to add admonition classes to directives.
|
||||
* `caution` and `tip` are not first-class PatternFly alert levels — map
|
||||
* them to the closest equivalent so PFAlert styles render correctly.
|
||||
*/
|
||||
export const remarkAdmonition: Plugin<[unknown], Root, VFile> = () => {
|
||||
const ADMONITION_LEVEL = {
|
||||
info: "pf-m-info",
|
||||
warning: "pf-m-warning",
|
||||
danger: "pf-m-danger",
|
||||
note: "pf-m-info",
|
||||
caution: "pf-m-warning",
|
||||
tip: "pf-m-success",
|
||||
} as const satisfies Record<AdmonitionType, string>;
|
||||
|
||||
/**
|
||||
* Remark plugin to convert `:::info` / `:::warning` / etc. directives
|
||||
* to `<ak-alert>` elements. The first child paragraph carrying the
|
||||
* `directiveLabel` flag (i.e. `:::info[Title]` syntax) is promoted to
|
||||
* a `<strong>` so the title renders inside the admonition slot.
|
||||
*/
|
||||
export const remarkAdmonition: Plugin<[], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
const visitor = (node: Directives) => {
|
||||
if (
|
||||
node.type === "containerDirective" ||
|
||||
node.type === "leafDirective" ||
|
||||
node.type === "textDirective"
|
||||
node.type !== "containerDirective" &&
|
||||
node.type !== "leafDirective" &&
|
||||
node.type !== "textDirective"
|
||||
) {
|
||||
if (!ADMONITION_TYPES.has(node.name)) return;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = node.data || (node.data = {});
|
||||
if (!isAdmonitionType(node.name)) return;
|
||||
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
const data = node.data || (node.data = {});
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
data.hName = tagName;
|
||||
|
||||
data.hName = tagName;
|
||||
const element = h(tagName, node.attributes || {});
|
||||
data.hProperties = element.properties || {};
|
||||
data.hProperties.level = ADMONITION_LEVEL[node.name] ?? `pf-m-${node.name}`;
|
||||
|
||||
const element = h(tagName, node.attributes || {});
|
||||
|
||||
data.hProperties = element.properties || {};
|
||||
data.hProperties.level = `pf-m-${node.name}`;
|
||||
const children = node.children as Paragraph[];
|
||||
const labelIndex = children.findIndex(
|
||||
(c) => c.type === "paragraph" && c.data?.directiveLabel,
|
||||
);
|
||||
if (labelIndex !== -1) {
|
||||
const label = children[labelIndex];
|
||||
children[labelIndex] = {
|
||||
type: "paragraph",
|
||||
children: [{ type: "strong", children: label.children }],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,3 +79,22 @@ export const remarkAdmonition: Plugin<[unknown], Root, VFile> = () => {
|
||||
visit(tree, visitor);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Match a Docusaurus-style admonition opening line:
|
||||
*
|
||||
* ```
|
||||
* :::info Title
|
||||
*```
|
||||
* `remark-directive` only understands the spec form `:::name[label]{attrs}`,
|
||||
* so a bare-space label silently falls through as plain text. Rewrite
|
||||
* the source so the directive parser sees the bracketed form.
|
||||
*/
|
||||
const ADMONITION_BARE_LABEL_RE = new RegExp(
|
||||
`^(:::(?:${[...ADMONITION_TYPES].join("|")}))[ \\t]+(.+?)[ \\t]*$`,
|
||||
"gm",
|
||||
);
|
||||
|
||||
export function normalizeAdmonitionLabels(source: string): string {
|
||||
return source.replace(ADMONITION_BARE_LABEL_RE, "$1[$2]");
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { VFile } from "vfile";
|
||||
/**
|
||||
* Remark plugin to add IDs to headings.
|
||||
*/
|
||||
export const remarkHeadings: Plugin<[unknown], Root, VFile> = () => {
|
||||
export const remarkHeadings: Plugin<[], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
const visitor = (node: Heading) => {
|
||||
const textContent = toString(node);
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { VFile } from "vfile";
|
||||
/**
|
||||
* Remark plugin to process lists.
|
||||
*/
|
||||
export const remarkLists: Plugin<[unknown], Root, VFile> = () => {
|
||||
export const remarkLists: Plugin<[], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
const visitor = (node: List) => {
|
||||
node.data = node.data || {};
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
:host {
|
||||
--ak-mermaid-message-text: var(--pf-c-content--Color);
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--light-200);
|
||||
}
|
||||
|
||||
/*
|
||||
* `<ak-alert>` deliberately does not set its own `:host { display }` —
|
||||
* every consumer is expected to set it (see e.g. captcha/styles.css).
|
||||
* Without this, the shadow-tree flex layout collapses into an inline
|
||||
* box and the admonition is unreadable.
|
||||
*/
|
||||
ak-alert {
|
||||
display: block;
|
||||
margin-block-start: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
ak-alert + p {
|
||||
margin-block-start: var(--pf-global--spacer--md);
|
||||
}
|
||||
@@ -59,37 +69,10 @@ pre:has(.hljs) {
|
||||
padding: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
svg[id^="mermaid-svg-"] {
|
||||
.rect {
|
||||
fill: var(
|
||||
--ak-mermaid-box-background-color,
|
||||
var(--pf-global--BackgroundColor--light-300)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
stroke-width: 4;
|
||||
fill: var(--ak-mermaid-message-text) !important;
|
||||
paint-order: stroke;
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Dark Theme */
|
||||
|
||||
:host([theme="dark"]) {
|
||||
--ak-mermaid-message-text: var(--ak-dark-foreground);
|
||||
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter);
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
|
||||
|
||||
svg[id^="mermaid-svg-"] {
|
||||
line[class^="messageLine"] {
|
||||
/*
|
||||
Mermaid's support for dynamic palette changes leaves a lot to be desired.
|
||||
This is a workaround to keep content readable while not breaking the rest of the theme.
|
||||
*/
|
||||
filter: invert(1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ifPresent } from "#elements/utils/attributes";
|
||||
import type { ThemedUrls } from "@goauthentik/api";
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
import { ImgHTMLAttributes } from "react";
|
||||
import type { ImgHTMLAttributes } from "react";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* @see https://github.com/atom/one-dark-syntax
|
||||
*/
|
||||
|
||||
:root {
|
||||
:root,
|
||||
:host {
|
||||
--one-dark-base: #282c34;
|
||||
--one-dark-mono-1: #abb2bf;
|
||||
--one-dark-mono-2: #818896;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Application } from "@goauthentik/api";
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
import { kebabCase } from "change-case";
|
||||
import { HTMLAttributes } from "react";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
198
web/test/browser/ak-mdx.test.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { expect, test } from "#e2e";
|
||||
import { randomName } from "#e2e/utils/generators";
|
||||
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
import { series } from "@goauthentik/core/promises";
|
||||
|
||||
/**
|
||||
* `<ak-mdx>` renders the OAuth 2.0 provider docs (`oauth2/index.mdx`) on
|
||||
* the OAuth2 provider view page. That document is well-suited to exercise
|
||||
* the full pipeline because it contains:
|
||||
*
|
||||
* - frontmatter (`title: OAuth 2.0 provider`)
|
||||
* - multiple H2 headings (id slugs)
|
||||
* - `:::caution` and `:::info` admonitions (two flavours: with title, without)
|
||||
* - relative-doc links (`./create-oauth2-provider.md`)
|
||||
* - external links (`https://oauth.net/2/`)
|
||||
* - a `mermaid` sequence diagram
|
||||
*
|
||||
* These tests boot the admin UI, create a fresh OAuth2 provider, navigate
|
||||
* to its view page, and then assert against the rendered DOM inside
|
||||
* `<ak-mdx>`'s shadow root.
|
||||
*/
|
||||
test.describe("ak-mdx renders compiled markdown", () => {
|
||||
let providerName: string;
|
||||
|
||||
test.beforeEach("Provision an OAuth2 provider", async ({ session, form, pointer, page }) => {
|
||||
const seed = IDGenerator.randomID(6);
|
||||
providerName = `${randomName(seed)} (${seed})`;
|
||||
|
||||
const { fill, selectSearchValue } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
await test.step("Authenticate", () => session.login({ to: "/if/admin/#/core/providers" }));
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "New Provider Wizard" });
|
||||
|
||||
await test.step("Create provider via wizard", async () => {
|
||||
await expect(dialog).toBeHidden();
|
||||
await page.getByRole("button", { name: "New Provider" }).click();
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await series(
|
||||
[click, "OAuth2/OpenID", "option"],
|
||||
[fill, "Provider Name", providerName],
|
||||
[
|
||||
selectSearchValue,
|
||||
"Authorization Flow",
|
||||
/default-provider-authorization-explicit-consent/,
|
||||
],
|
||||
[click, "Create", "button", dialog],
|
||||
);
|
||||
|
||||
await expect(dialog).toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Navigate to the provider's view page", async () => {
|
||||
const $row = await form.search(providerName);
|
||||
// The provider name cell is a link that opens the view page.
|
||||
await $row.getByRole("link", { name: providerName }).first().click();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns a Locator scoped to the rendered `<ak-mdx>` element on the
|
||||
* provider view page (there is exactly one inside the docs card).
|
||||
*/
|
||||
const $mdx = (page: import("@playwright/test").Page) =>
|
||||
page.locator("ak-mdx").filter({ has: page.locator('h1[part="title"]') });
|
||||
|
||||
test("frontmatter title and heading slugs are rendered", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
await expect(
|
||||
mdx.locator('h1[part="title"]'),
|
||||
"Frontmatter `title` rendered as an `<h1 part=title>`",
|
||||
).toHaveText("OAuth 2.0 provider");
|
||||
|
||||
await expect(
|
||||
mdx.locator("h2#authentik-and-oauth-2-0"),
|
||||
"H2 carries a kebab-cased id slug derived from its text",
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
mdx.locator("h2#about-oauth-2-0-and-oidc"),
|
||||
"Multiple H2s each receive their own slug",
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("admonitions render as <ak-alert> with the right level", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
const $caution = mdx
|
||||
.locator('ak-alert[level="pf-m-warning"]')
|
||||
.filter({ hasText: "Reserved application slugs" });
|
||||
await expect(
|
||||
$caution,
|
||||
"`:::caution Title` renders an `<ak-alert level=pf-m-warning>` with the title in `<strong>`",
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
$caution.locator("strong"),
|
||||
"Bare-space directive label is promoted to `<strong>`",
|
||||
).toHaveText("Reserved application slugs");
|
||||
|
||||
await expect(
|
||||
mdx.locator('ak-alert[level="pf-m-info"]').first(),
|
||||
"`:::info` blocks render as `<ak-alert level=pf-m-info>`",
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("links are wrapped in <ak-md-a> with build-time URL resolution", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
const $external = mdx.locator('ak-md-a > a[href="https://oauth.net/2/"]');
|
||||
await expect($external, "External link preserved verbatim").toBeVisible();
|
||||
await expect($external).toHaveAttribute("target", "_blank");
|
||||
await expect($external).toHaveAttribute("rel", "noopener noreferrer");
|
||||
|
||||
const $relative = mdx
|
||||
.locator('ak-md-a > a[href*="next.goauthentik.io"][href*="create-oauth2-provider"]')
|
||||
.first();
|
||||
await expect(
|
||||
$relative,
|
||||
"Relative `./create-oauth2-provider.md` resolved to docs site URL at build time",
|
||||
).toBeVisible();
|
||||
await expect($relative).toHaveAttribute("target", "_blank");
|
||||
|
||||
// Fragment href is preserved verbatim from the source markdown,
|
||||
// even when (as here) the docs author's intended target slug
|
||||
// doesn't match this pipeline's slug algorithm. The wrapper
|
||||
// intercepts the click regardless — the lookup only fails the
|
||||
// scroll, not the link itself.
|
||||
const $fragment = mdx.locator('ak-md-a > a[href="#about-oauth-20-and-oidc"]').first();
|
||||
await expect(
|
||||
$fragment,
|
||||
"Fragment links are kept as `#…` so the wrapper can intercept them",
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
$fragment,
|
||||
"Fragment links do NOT receive `target=_blank`",
|
||||
).not.toHaveAttribute("target", "_blank");
|
||||
});
|
||||
|
||||
test("mermaid diagrams render via <ak-diagram>", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
const $diagram = mdx.locator("ak-diagram").first();
|
||||
await expect($diagram).toBeVisible();
|
||||
|
||||
const $svg = $diagram.locator("svg");
|
||||
await expect(
|
||||
$svg,
|
||||
"<ak-diagram> resolves the mermaid SVG into its shadow root",
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test("mermaid responds to theme changes", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
const $svg = mdx.locator("ak-diagram svg").first();
|
||||
await expect($svg).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// `<ak-diagram>` re-renders the whole SVG via `mermaid.render(...)` on
|
||||
// every `AKMermaidRefreshEvent`. Mermaid bakes the active theme into
|
||||
// an inline `<style>` block inside the SVG, so the easiest stable
|
||||
// signal that the right theme was applied is to assert the
|
||||
// serialized SVG content changes between toggles.
|
||||
const captureSVG = () => $svg.evaluate((el) => el.outerHTML);
|
||||
const darkSVG = await captureSVG();
|
||||
expect(darkSVG.length, "Initial mermaid SVG is non-empty").toBeGreaterThan(0);
|
||||
|
||||
await test.step("Toggle to light theme", async () => {
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.dataset.themeChoice = "light";
|
||||
});
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(captureSVG, {
|
||||
message: "SVG content should change when re-rendered for light theme",
|
||||
timeout: 10_000,
|
||||
})
|
||||
.not.toBe(darkSVG);
|
||||
|
||||
const lightSVG = await captureSVG();
|
||||
|
||||
await test.step("Toggle back to dark theme", async () => {
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.dataset.themeChoice = "dark";
|
||||
});
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(captureSVG, {
|
||||
message: "SVG content should change again when re-rendered for dark theme",
|
||||
timeout: 10_000,
|
||||
})
|
||||
.not.toBe(lightSVG);
|
||||
});
|
||||
});
|
||||
37
web/types/mdx.d.ts
vendored
@@ -1,39 +1,22 @@
|
||||
/**
|
||||
* @file Provides types for ESBuild "virtual modules" generated from MDX files.
|
||||
* @file Provides types for ESBuild "virtual modules" generated from
|
||||
* Markdown / MDX files. The bundler's `mdx-plugin` compiles these to
|
||||
* HTML at build time and emits a JSON envelope; importing the file
|
||||
* yields the URL of that JSON envelope.
|
||||
*/
|
||||
|
||||
declare module "~docs/types" {
|
||||
/**
|
||||
* A parsed JSON module containing MDX content and metadata from ESBuild.
|
||||
*/
|
||||
export interface MDXModule {
|
||||
/**
|
||||
* The Markdown content of the module.
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* The public path of the module, typically identical to the docs page path.
|
||||
*/
|
||||
publicPath?: string;
|
||||
/**
|
||||
* The public directory of the module, used to resolve relative links.
|
||||
*/
|
||||
publicDirectory?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "~docs/*.md" {
|
||||
/**
|
||||
* The serialized JSON content of an MD file.
|
||||
* URL of the JSON envelope emitted for the imported file.
|
||||
*/
|
||||
const serializedJSON: string;
|
||||
export default serializedJSON;
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
declare module "~docs/*.mdx" {
|
||||
/**
|
||||
* The serialized JSON content of an MDX file.
|
||||
* URL of the JSON envelope emitted for the imported file.
|
||||
*/
|
||||
const serializedJSON: string;
|
||||
export default serializedJSON;
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
@@ -5270,7 +5270,7 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
|
||||
<target>Aktivovat</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>Aktualizovat heslo uživatele <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11007,6 +11007,22 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5295,7 +5295,7 @@ Hier können nur Policies verwendet werden, da der Zugriff geprüft wird, bevor
|
||||
<target>Aktivieren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target><x id="0" equiv-text="${item.name || item.username}"/> - Passwort ändern.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11040,6 +11040,22 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4089,7 +4089,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Activate</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -9010,6 +9010,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5235,7 +5235,7 @@ El valor de este campo se compara con el atributo de pertenencia del usuario.</t
|
||||
<target>Activar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>Actualizar la contraseña de <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -10965,6 +10965,22 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5398,7 +5398,7 @@ läpäisy estyy kun jompi kumpi tai molemmat vaihtoehdot ylittävät raja-arvon.
|
||||
<target>Aktivoi</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>Päivitä käyttäjän <x id="0" equiv-text="${item.name || item.username}"/> salasana</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11206,6 +11206,22 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5388,7 +5388,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>Activer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>Mettre à jour le mot de passe de <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11195,6 +11195,22 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5194,7 +5194,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>Attivare</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>Aggiorna <x id="0" equiv-text="${item.name || item.username}"/> password</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -10914,6 +10914,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5395,7 +5395,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>アクティブ化</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target><x id="0" equiv-text="${item.name || item.username}"/> のパスワードを更新</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11195,6 +11195,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4988,7 +4988,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>활성화</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target><x id="0" equiv-text="${item.name || item.username}"/> 비밀번호 업데이트 </target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -10562,6 +10562,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4804,7 +4804,7 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
|
||||
<target>Activeren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10246,6 +10246,22 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5004,7 +5004,7 @@ Można tu używać tylko zasad, ponieważ dostęp jest sprawdzany przed uwierzyt
|
||||
<target>Aktywuj</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10588,6 +10588,22 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5394,7 +5394,7 @@ Você só pode usar políticas aqui, pois o acesso é verificado antes de o usu
|
||||
<target>Ativar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>Atualizar a senha de <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11188,6 +11188,22 @@ por exemplo: <x id="0" equiv-text="<code>"/>oci://registry.domain.tld/path
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5052,7 +5052,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>Активировать</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10674,6 +10674,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5051,7 +5051,7 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
|
||||
<target>Etkinleştir</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10663,6 +10663,22 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -5461,7 +5461,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>激活</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
<target>更新 <x id="0" equiv-text="${user.name || user.username}"/> 的密码</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
@@ -11467,6 +11467,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -4839,7 +4839,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<target>啟用</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${user.name || user.username}"/>'s password</source>
|
||||
<source>Update <x id="0" equiv-text="${formatUserDisplayName(user)}"/>'s password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sce8d867ca5f35304">
|
||||
<source>Set password</source>
|
||||
@@ -10299,6 +10299,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sf7aba95a8c43b7b1">
|
||||
<source>Sets a custom EntityID/Issuer to override the authentik generated default.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3a27a128ad87f31">
|
||||
<source>Passwords</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s16d13ea527d7fe6b">
|
||||
<source>Setting</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfef81bb4077a56fd">
|
||||
<source>Type a new password...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9ec917e3e986bc1">
|
||||
<source>When enabled, your username will be remembered on this device for future logins.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.submitting.no-entity">
|
||||
<source><x id="0" equiv-text="${submittingVerb}"/>...</source>
|
||||
<note from="lit-localize">The message shown while a form is being submitted, when no entity name is provided.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
234
website/integrations/development/ghec-emu/index.mdx
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Managed Users
|
||||
sidebar_label: GitHub Enterprise EMU
|
||||
support_level: community
|
||||
---
|
||||
|
||||
import TabItem from "@theme/TabItem";
|
||||
import Tabs from "@theme/Tabs";
|
||||
|
||||
## What is GitHub Enterprise Managed Users
|
||||
|
||||
> With Enterprise Managed Users, you manage the lifecycle and authentication of your users on GitHub from an external identity management system, or IdP.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/understanding-iam-for-enterprises/about-enterprise-managed-users
|
||||
|
||||
This guide configures authentik as the SAML identity provider and SCIM provider for GitHub Enterprise Cloud with Enterprise Managed Users (EMU). It applies to EMU enterprises hosted on GitHub.com and EMU enterprises with data residency on GHE.com.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/enterprises/foo` is your GitHub.com EMU enterprise, where `foo` is the name of your enterprise.
|
||||
- `foo.ghe.com` is your GHE.com EMU enterprise, where `foo` is the name of your enterprise.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub enterprise administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
SCIM must be configured for this integration. GitHub matches the SAML identity to the SCIM identity by comparing the SAML `NameID` value with the SCIM `userName` value. The mappings below use the `github_emu_username` user attribute when it exists, and fall back to the authentik username.
|
||||
|
||||
Use the values for your EMU deployment when configuring authentik:
|
||||
|
||||
<Tabs
|
||||
groupId="github-emu-deployment"
|
||||
defaultValue="github"
|
||||
values={[
|
||||
{label: 'GitHub.com', value: 'github'},
|
||||
{label: 'GHE.com', value: 'ghec'},
|
||||
]}>
|
||||
<TabItem value="github">
|
||||
|
||||
| Setting | Value |
|
||||
| ------------ | ------------------------------------------------- |
|
||||
| **ACS URL** | `https://github.com/enterprises/foo/saml/consume` |
|
||||
| **Audience** | `https://github.com/enterprises/foo` |
|
||||
| **Issuer** | `https://github.com/enterprises/foo` |
|
||||
| **SCIM URL** | `https://api.github.com/scim/v2/enterprises/foo` |
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ghec">
|
||||
|
||||
| Setting | Value |
|
||||
| ------------ | -------------------------------------------------- |
|
||||
| **ACS URL** | `https://foo.ghe.com/enterprises/foo/saml/consume` |
|
||||
| **Audience** | `https://foo.ghe.com/enterprises/foo` |
|
||||
| **Issuer** | `https://foo.ghe.com/enterprises/foo` |
|
||||
| **SCIM URL** | `https://api.foo.ghe.com/scim/v2/enterprises/foo` |
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise EMU with authentik, you need to create property mappings, an application/provider pair, application entitlements, and a SCIM provider.
|
||||
|
||||
### Create property mappings in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
3. Create the following **SAML Provider Property Mapping**s:
|
||||
- **Name**: `GitHub EMU username`
|
||||
- **SAML Attribute Name**: `http://schemas.goauthentik.io/2021/02/saml/username`
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
return request.user.attributes.get("github_emu_username", request.user.username)
|
||||
```
|
||||
|
||||
- **Name**: `GitHub EMU full name`
|
||||
- **SAML Attribute Name**: `full_name`
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
return request.user.name
|
||||
```
|
||||
|
||||
- **Name**: `GitHub EMU emails`
|
||||
- **SAML Attribute Name**: `emails`
|
||||
- **Expression**:
|
||||
|
||||
```python
|
||||
if request.user.email:
|
||||
yield request.user.email
|
||||
```
|
||||
|
||||
4. Create a **SCIM Provider Mapping** with the following settings:
|
||||
- **Name**: `GitHub EMU user`
|
||||
- **Expression**:
|
||||
|
||||
The supported `roles` values are documented in [GitHub Enterprise Cloud's SCIM API documentation](https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-user).
|
||||
|
||||
```python
|
||||
username = request.user.attributes.get("github_emu_username", request.user.username)
|
||||
formatted = request.user.name or username
|
||||
given_name = formatted
|
||||
family_name = " "
|
||||
if " " in formatted:
|
||||
given_name, _, family_name = formatted.partition(" ")
|
||||
|
||||
emails = []
|
||||
if request.user.email:
|
||||
emails.append(
|
||||
{
|
||||
"value": request.user.email,
|
||||
"type": "work",
|
||||
"primary": True,
|
||||
}
|
||||
)
|
||||
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({"value": "enterprise_owner", "primary": True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({"value": "user", "primary": True})
|
||||
|
||||
return {
|
||||
"userName": username,
|
||||
"externalId": str(request.user.uid),
|
||||
"name": {
|
||||
"formatted": formatted,
|
||||
"givenName": given_name,
|
||||
"familyName": family_name,
|
||||
},
|
||||
"displayName": formatted,
|
||||
"active": request.user.is_active,
|
||||
"emails": emails,
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set **ACS URL** to the ACS URL for your EMU deployment.
|
||||
- Set **Audience** to the audience value for your EMU deployment.
|
||||
- Set **Issuer** to the issuer value for your EMU deployment.
|
||||
- Set **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**:
|
||||
- Add the `GitHub EMU full name` and `GitHub EMU emails` property mappings.
|
||||
- Set **NameID Property Mapping** to `GitHub EMU username`.
|
||||
- Set **Default NameID Policy** to `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`.
|
||||
- Select an available **Signing certificate**. Download this certificate because it is required later.
|
||||
- Enable **Sign assertion** and **Sign response**.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application are synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create application entitlements
|
||||
|
||||
1. In the authentik Admin interface, open the GitHub EMU application that you created.
|
||||
2. Click the **Application entitlements** tab.
|
||||
3. Create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
4. Open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
## GitHub configuration
|
||||
|
||||
When GitHub provisions your managed enterprise, GitHub sends an email inviting you to reset the password for the setup user. The setup user has the username `foo_admin`, cannot be linked with SSO, and is the emergency account that can bypass SSO requirements.
|
||||
|
||||
### Create the SCIM token
|
||||
|
||||
1. Log in as the setup user.
|
||||
2. Navigate to the personal access tokens page:
|
||||
- GitHub.com: `https://github.com/settings/tokens`
|
||||
- GHE.com: `https://foo.ghe.com/settings/tokens`
|
||||
3. Generate a new classic personal access token with the `scim:enterprise` scope.
|
||||
4. Copy the token. This value is used in the authentik SCIM provider.
|
||||
|
||||
### Configure SAML in GitHub
|
||||
|
||||
1. Log in as the setup user.
|
||||
2. Navigate to your enterprise.
|
||||
3. Click **Identity provider**.
|
||||
4. Under **Identity Provider**, click **Single sign-on configuration**.
|
||||
5. Under **Open SCIM Configuration**, select **Enable open SCIM configuration**.
|
||||
6. Under **SAML single sign-on**, select **Add SAML configuration**.
|
||||
7. Configure the following settings:
|
||||
- **Sign on URL**: enter the **SSO URL (Redirect)** from the SAML provider that you created in authentik.
|
||||
- **Issuer**: enter the **Issuer** that you configured in authentik.
|
||||
- **Public certificate**: paste the full signing certificate that you downloaded from authentik.
|
||||
- **Signature method** and **Digest method**: select the methods that match the authentik SAML provider settings.
|
||||
8. Click **Test SAML configuration**.
|
||||
9. After the test succeeds, click **Save SAML settings**.
|
||||
10. Save the SAML recovery codes that GitHub provides.
|
||||
|
||||

|
||||
|
||||
### Create a SCIM provider in authentik
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** > **Providers** and click **Create**.
|
||||
2. Select **SCIM Provider** as the provider type and click **Next**.
|
||||
3. Configure the following settings:
|
||||
- **Name**: provide a descriptive name.
|
||||
- **URL**: enter the SCIM URL for your EMU deployment.
|
||||
- **Token**: paste the GitHub personal access token that you created earlier.
|
||||
- **User Property Mappings**: remove `authentik default SCIM Mapping: User`, then add the `GitHub EMU user` mapping that you created earlier.
|
||||
- **Group Property Mappings**: keep `authentik default SCIM Mapping: Group` selected.
|
||||
4. Click **Finish**.
|
||||
5. Navigate to **Applications** > **Applications** and open the GitHub EMU application.
|
||||
6. Add the SCIM provider to **Backchannel Providers**.
|
||||
7. Click **Update**.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with GitHub Enterprise EMU, assign a test user to the `GitHub Users` entitlement and ensure that the user can view the application in authentik.
|
||||
|
||||
Open the SCIM provider and click **Run sync again**. After the sync completes, confirm that the user is provisioned in GitHub. Then, log in to GitHub as the test user and confirm that GitHub redirects the user to authentik for SAML authentication.
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Enterprise Cloud: configuring SAML single sign-on for Enterprise Managed Users](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/configuring-authentication-for-enterprise-managed-users/configuring-saml-single-sign-on-for-enterprise-managed-users)
|
||||
- [GitHub Enterprise Cloud: configuring SCIM provisioning for Enterprise Managed Users](https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/provisioning-user-accounts-with-scim/configuring-scim-provisioning-for-users)
|
||||
- [GitHub Enterprise Cloud: REST API endpoints for SCIM](https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/scim)
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
75
website/integrations/development/ghec/index.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Cloud
|
||||
sidebar_label: GitHub Enterprise Cloud
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Cloud
|
||||
|
||||
> GitHub Enterprise Cloud is a plan for large businesses or teams who collaborate on GitHub.com.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/get-started/learning-about-github/githubs-plans
|
||||
|
||||
This guide configures SAML SSO for a GitHub Enterprise Cloud organization.
|
||||
|
||||
:::info
|
||||
For GitHub Enterprise Cloud with Enterprise Managed Users, see the [GitHub Enterprise EMU](../ghec-emu/) integration guide.
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/orgs/foo` is your GitHub organization, where `foo` is the name of your organization.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Cloud with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set **ACS URL** to `https://github.com/orgs/foo/saml/consume`.
|
||||
- Set **Audience** to `https://github.com/orgs/foo`.
|
||||
- Set **Issuer** to `https://github.com/orgs/foo`.
|
||||
- Set **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. Download this certificate because it is required later.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## GitHub configuration
|
||||
|
||||
1. Log in to GitHub as an organization owner.
|
||||
2. Navigate to your organization at `https://github.com/foo`.
|
||||
3. Click **Settings**.
|
||||
4. In the left sidebar, under **Security**, click **Authentication security**.
|
||||
5. Under **SAML single sign-on**, select **Enable SAML authentication**.
|
||||
6. Configure the following settings:
|
||||
- **Sign on URL**: enter the **SSO URL (Redirect)** from the SAML provider that you created in authentik.
|
||||
- **Issuer**: enter the **Issuer** that you configured in authentik.
|
||||
- **Public certificate**: paste the full signing certificate that you downloaded from authentik.
|
||||
- **Signature method** and **Digest method**: select the methods that match the authentik SAML provider settings.
|
||||
7. Click **Test SAML configuration**.
|
||||
8. After the test succeeds, click **Save**.
|
||||
|
||||

|
||||
|
||||
This enables SAML as an authentication option. To require SAML for all organization members, visit `https://github.com/orgs/foo/sso`, sign in with SAML, then return to **Authentication security** and select **Require SAML SSO authentication for all members of the foo organization**.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with GitHub Enterprise Cloud, log out of GitHub and then access a resource in the organization. GitHub should prompt you to authenticate with SAML through authentik.
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Enterprise Cloud: managing SAML single sign-on for your organization](https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-saml-single-sign-on-for-your-organization)
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
142
website/integrations/development/ghes/index.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Server
|
||||
sidebar_label: GitHub Enterprise Server
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Server
|
||||
|
||||
> GitHub Enterprise Server is a self-hosted platform for software development within your enterprise.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-server@latest/admin/overview/about-github-enterprise-server
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.company` is the FQDN of your GitHub Enterprise Server installation.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub Enterprise Server users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub Enterprise Server administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Server with authentik, you need to create an application/provider pair in authentik. If you want to use SCIM provisioning, you also need to create application entitlements and a SCIM property mapping.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set **ACS URL** to `https://github.company/saml/consume`.
|
||||
- Set **Audience** to `https://github.company`.
|
||||
- Set **Issuer** to `https://github.company`.
|
||||
- Set **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**:
|
||||
- Select an available **Signing certificate**. Download this certificate because it is required later.
|
||||
- Set **NameID Property Mapping** to `authentik default SAML Mapping: Username`.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application are synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create application entitlements
|
||||
|
||||
1. In the authentik Admin interface, open the GitHub Enterprise Server application that you created.
|
||||
2. Click the **Application entitlements** tab.
|
||||
3. Create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
4. Open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
### Create a SCIM property mapping
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
2. Select **SCIM Provider Mapping** and click **Next**.
|
||||
3. Create a mapping for GitHub roles:
|
||||
- **Name**: `GitHub roles`
|
||||
- **Expression**:
|
||||
|
||||
The supported `roles` values are documented in [GitHub Enterprise Server's SCIM API documentation](https://docs.github.com/en/enterprise-server@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-user).
|
||||
|
||||
```python
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({"value": "enterprise_owner", "primary": True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({"value": "user", "primary": True})
|
||||
|
||||
return {
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
4. Click **Finish**.
|
||||
|
||||
## GitHub Enterprise Server configuration
|
||||
|
||||
### Create the SCIM token
|
||||
|
||||
1. Log in to GitHub Enterprise Server with the administrator account that you use for SCIM provisioning.
|
||||
2. Navigate to `https://github.company/settings/tokens`.
|
||||
3. Generate a new classic personal access token with the `scim:enterprise` scope.
|
||||
4. Copy the token. This value is used in the authentik SCIM provider.
|
||||
|
||||
### Configure SAML
|
||||
|
||||
1. Navigate to the GitHub Enterprise Server Management Console at `https://github.company:8443`.
|
||||
2. Sign in as an administrator.
|
||||
3. Go to **Authentication**.
|
||||
4. Configure the following settings:
|
||||
- Select **SAML**.
|
||||
- **Sign on URL**: enter the **SSO URL (Redirect)** from the SAML provider that you created in authentik.
|
||||
- **Issuer**: enter the **Issuer** that you configured in authentik.
|
||||
- **Signature method** and **Digest method**: select the methods that match the authentik SAML provider settings.
|
||||
- **Validation certificate**: upload the signing certificate that you downloaded from authentik.
|
||||
- If you plan to use SCIM, select **Allow creation of accounts with built-in authentication** and **Disable administrator demotion/promotion**.
|
||||
- In the **User attributes** section, do not configure a different username attribute unless it returns the same value as the SCIM `userName` attribute.
|
||||
5. Click **Save settings** and wait for the changes to apply.
|
||||
|
||||

|
||||
|
||||
### Enable SCIM
|
||||
|
||||
1. Log in to GitHub Enterprise Server with an administrator account.
|
||||
2. Open **Enterprise settings**.
|
||||
3. In the left sidebar, click **Settings** > **Authentication security**.
|
||||
4. Select **Enable SCIM configuration**.
|
||||
5. Click **Save**.
|
||||
|
||||
### Create a SCIM provider in authentik
|
||||
|
||||
1. In the authentik Admin interface, navigate to **Applications** > **Providers** and click **Create**.
|
||||
2. Select **SCIM Provider** as the provider type and click **Next**.
|
||||
3. Configure the following settings:
|
||||
- **Name**: provide a descriptive name.
|
||||
- **URL**: `https://github.company/api/v3/scim/v2`
|
||||
- **Token**: paste the GitHub personal access token that you created earlier.
|
||||
- **User Property Mappings**: keep `authentik default SCIM Mapping: User` selected, then add the `GitHub roles` mapping that you created earlier.
|
||||
- **Group Property Mappings**: keep `authentik default SCIM Mapping: Group` selected.
|
||||
4. Click **Finish**.
|
||||
5. Navigate to **Applications** > **Applications** and open the GitHub Enterprise Server application.
|
||||
6. Add the SCIM provider to **Backchannel Providers**.
|
||||
7. Click **Update**.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with GitHub Enterprise Server, assign a test user to the `GitHub Users` entitlement and ensure that the user can view the application in authentik.
|
||||
|
||||
Open the SCIM provider and click **Run sync again**. After the sync completes, confirm that the user is provisioned in GitHub Enterprise Server. Then, log in to GitHub Enterprise Server as the test user and confirm that GitHub redirects the user to authentik for SAML authentication.
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Enterprise Server: configuring SAML single sign-on for your enterprise](https://docs.github.com/en/enterprise-server@latest/admin/managing-iam/using-saml-for-enterprise-iam/configuring-saml-single-sign-on-for-your-enterprise)
|
||||
- [GitHub Enterprise Server: REST API endpoints for SCIM](https://docs.github.com/en/enterprise-server@latest/rest/enterprise-admin/scim)
|
||||
|
Before Width: | Height: | Size: 60 KiB |
@@ -1,67 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Cloud
|
||||
sidebar_label: GitHub Enterprise Cloud
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Cloud
|
||||
|
||||
> GitHub is a complete developer platform to build, scale, and deliver secure software. Businesses use our suite of products to support the entire software development lifecycle, increasing development velocity and improving code quality.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/admin/overview/about-github-for-enterprises
|
||||
|
||||
:::info
|
||||
GitHub Enterprise Cloud EMU (Enterprise Managed Users) are not compatible with authentik. GitHub currently only permits SAML/OIDC for EMU organizations with Okta and/or Microsoft Entra ID (Azure AD).
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/enterprises/foo` is your GitHub organization, where `foo` is the name of your enterprise.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Cloud with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.com/enterprises/foo/saml/consume`.
|
||||
- Set the **Audience** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Issuer** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
Navigate to your enterprise settings by clicking your GitHub user portrait in the top right of GitHub.com, then select `Your enterprises` and click `Settings` for the enterprise you wish to configure.
|
||||
|
||||
In the left-hand navigation, within the `Settings` section, click `Authentication security`.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the `Require SAML authentication` checkbox.
|
||||
- In `Sign on URL`, type `https://authentik.company/application/saml/<application_slug>/sso/binding/redirect/`
|
||||
- For `Issuer`, type `https://github.com/enterprises/foo` or the `Audience` you set in authentik
|
||||
- For `Public certificate`, paste the _full_ signing certificate into this field.
|
||||
- Verify that the `Signature method` and `Digest method` match your SAML provider settings in authentik.
|
||||
|
||||

|
||||
|
||||
Once these fields are populated, you can use the `Test SAML configuration` button to test the authentication flow. If the flow completes successfully, you will see a green tick next to the Test button.
|
||||
|
||||
Scroll down to hit the `Save` button below.
|
||||
@@ -1,130 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Cloud - Enterprise Managed Users
|
||||
sidebar_label: GitHub Enterprise Cloud EMU
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Cloud - Enterprise Managed Users
|
||||
|
||||
> With Enterprise Managed Users, you manage the lifecycle and authentication of your users on GitHub from an external identity management system, or IdP:
|
||||
>
|
||||
> - Your IdP provisions new user accounts on GitHub, with access to your enterprise.
|
||||
> - Users must authenticate on your IdP to access your enterprise's resources on GitHub.
|
||||
> - You control usernames, profile data, organization membership, and repository access from your IdP.
|
||||
> - If your enterprise uses OIDC SSO, GitHub will validate access to your enterprise and its resources using your IdP's Conditional Access Policy (CAP). See "About support for your IdP's Conditional Access Policy."
|
||||
> - Managed user accounts cannot create public content or collaborate outside your enterprise. See "Abilities and restrictions of managed user accounts."
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/understanding-iam-for-enterprises/about-enterprise-managed-users
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/enterprises/foo` is your GitHub organization, where `foo` is the name of your enterprise
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub Enterprise Cloud EMU users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub enterprise administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Cloud EMU with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
:::info
|
||||
In order to use GitHub Enterprise Cloud EMU, SCIM must also be set up.
|
||||
:::
|
||||
|
||||
:::info
|
||||
GitHub will create usernames for your EMU users based on the SAML `NameID` property, which must also match SCIM's `_userName_` attribute.
|
||||
:::
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.com/enterprises/foo/saml/consume`.
|
||||
- Set the **Audience** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Issuer** to `https://github.com/enterprises/foo`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- Under **NameID Property Mapping**, set **NameID Property Mapping** to be based on the `Email` field.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application will be synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
**Create the user and administrator entitlements**
|
||||
|
||||
In the authentik Admin interface, open the GitHub EMU application that you just created, click the **Application entitlements** tab, and create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
|
||||
After creating the entitlements, open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
## GitHub SAML Configuration
|
||||
|
||||
When your EMU is provisioned by GitHub, you will receive an email inviting you to reset the password of your 'setup user'. This user cannot be linked with SSO and is an emergency access account, as it will be the only account that can bypass SSO requirements.
|
||||
|
||||
Before enabling SAML, go to your [Personal access tokens](https://github.com/settings/tokens) on your EMU setup user and Generate a new _personal access token (classic)_. This should have a descriptive note like `SCIM Token`. It is advisable to set this to not expire. For scopes, select only _admin:enterprise_ and click _Generate token_.
|
||||
|
||||
Copy the resulting token to a safe location.
|
||||
|
||||
After you have set a password for this account and generated your SCIM token, navigate to your enterprise settings by clicking your GitHub user portrait in the top right of GitHub.com, select `Your enterprise`, click the `Settings` link, and then click `Authentication security`.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the `Require SAML authentication` checkbox.
|
||||
- In `Sign on URL`, input the _SSO URL (Redirect)_ entry from the SAML provider you created.
|
||||
- For `Issuer`, input the `Issuer` you set in authentik.
|
||||
- For `Public certificate`, paste the _full_ signing certificate into this field.
|
||||
- Verify that the `Signature method` and `Digest method` match your SAML provider settings in authentik.
|
||||
|
||||

|
||||
|
||||
Once these fields are populated, you can use the `Test SAML configuration` button to test the authentication flow. If the flow completes successfully, you will see a green tick next to the Test button.
|
||||
|
||||
Scroll down to hit the `Save SAML settings` button below.
|
||||
|
||||
You will now be prompted to save your SAML recovery codes. These will be necessary if you need to disable or change your SAML settings, so keep them safe!
|
||||
|
||||
## SCIM Provider
|
||||
|
||||
Before we create a SCIM provider, we also have to create a new Property Mapping. In authentik, go to _Customization_, then _Property Mappings_. Here, click _Create_, select _SCIM Provider Mapping_. Name the mapping something memorable and paste the following code in the _Expression_ field:
|
||||
|
||||
```python
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
# Edit this if statement if you need to add more GitHub roles.
|
||||
# Valid roles include:
|
||||
# user, guest_collaborator, enterprise_owner, billing_manager
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({'value': 'enterprise_owner', 'primary': True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({'value': 'user', 'primary': True})
|
||||
return {
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
If you renamed either entitlement, make sure that you update the code above to match.
|
||||
|
||||
Create a new SCIM provider with the following parameters:
|
||||
|
||||
- URL: `https://api.github.com/scim/v2/enterprises/foo/` (Replacing `foo` with your Enterprise slug.)
|
||||
- Token: Paste the token provided from GitHub here.
|
||||
- In the _Attribute mapping_ section, de-select the `authentik default SCIM Mapping: User` mapping by selecting it on the right-hand side and clicking the left-facing single chevron.
|
||||
- Select the property mapping you created in the previous step and add it by clicking the right-facing single chevron.
|
||||
- You can leave the _Group Property Mappings_ as is.
|
||||
- Click _Finish_.
|
||||
|
||||
Go back to your GitHub EMU Application created in the first step and add your new SCIM provider in the _Backchannel Providers_ field, then click the _Update_ button.
|
||||
|
||||
You should now be ready to assign users or groups to your _GitHub Users_ and _GitHub Admins_ application entitlements. Use application bindings or policies to limit which users can view the application and are synchronized by SCIM, and use the entitlements to assign the corresponding GitHub SCIM role values. If you do not see your users being provisioned, go to your SCIM provider and click the _Run sync again_ option. A few seconds later, you should see results of the SCIM sync.
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Enterprise Server
|
||||
sidebar_label: GitHub Enterprise Server
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is GitHub Enterprise Server
|
||||
|
||||
> GitHub Enterprise Server is a self-hosted platform for software development within your enterprise. Your team can use GitHub Enterprise Server to build and ship software using Git version control, powerful APIs, productivity and collaboration tools, and integrations. Developers familiar with GitHub.com can onboard and contribute seamlessly using familiar features and workflows.
|
||||
>
|
||||
> -- https://docs.github.com/en/enterprise-server@3.5/admin/overview/about-github-enterprise-server
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `https://github.company` is your GitHub Enterprise Server installation
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
- `GitHub Users` is an application entitlement used for standard GitHub Enterprise Server users.
|
||||
- `GitHub Admins` is an application entitlement used for GitHub Enterprise Server administrators.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Enterprise Server with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
:::info
|
||||
In order to use GitHub Enterprise Server, SCIM must also be set up.
|
||||
:::
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.company/saml/consume`.
|
||||
- Set the **Audience** and **Issuer** to `https://github.company`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page. If you add the SCIM provider as a backchannel provider later, only users who can view this application will be synchronized.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
### Create the user and administrator entitlements
|
||||
|
||||
In the authentik Admin interface, open the GitHub Enterprise Server application that you just created, click the **Application entitlements** tab, and create two entitlements named `GitHub Users` and `GitHub Admins`.
|
||||
|
||||
After creating the entitlements, open each entitlement and bind the users or groups that should receive it.
|
||||
|
||||
## SAML Configuration
|
||||
|
||||
If you plan to use SCIM (available from GHES 3.14.0), create a first administrator user on your instance and go to your personal access tokens at `https://github.company/settings/tokens/new`, click _Generate new token_, and then click _Generate new token (classic)_. Your token should have a descriptive name and, ideally, no expiration date. For permission scopes, you need to select _admin:enterprise_. Click _Generate token_ and store the resulting token in a safe location.
|
||||
|
||||
To enable SAML, navigate to your appliance maintenance settings. These are found at `https://github.company:8443`. Here, sign in with an administrator user and go to the Authentication section.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the _SAML_ option.
|
||||
- In _Sign on URL_, input your _SSO URL (Redirect)_ from authentik.
|
||||
- For _Issuer_, use the _Audience_ you set in authentik.
|
||||
- Verify that the _Signature method_ and _Digest method_ match your SAML provider settings in authentik.
|
||||
- For _Validation certificate_, upload the signing certificate you downloaded after creating the provider.
|
||||
- If you plan to enable SCIM, select _Allow creation of accounts with built-in authentication_ and _Disable administrator demotion/promotion_ options. These are selected so you can use your administrator user as an emergency non-SSO account, as well as create machine users, and to ensure users are not promoted outside your IdP.
|
||||
- In the _User attributes_ section, enter `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` in the _Username_ field to ensure the emails become normalized into usernames in GitHub.
|
||||
- Press Save settings on the left-hand side and wait for the changes to apply.
|
||||
|
||||

|
||||
|
||||
Once the appliance has saved the settings and reloaded the services, you should be able to navigate to your instance URL at `https://github.company` and sign in with SAML.
|
||||
|
||||
## SCIM Configuration
|
||||
|
||||
This section only applies if you completed the steps above to prepare the instance for SCIM enablement.
|
||||
|
||||
After enabling SAML, log into your initial administrator account again. Click the user portrait in the top right, click _Enterprise settings_, click _Settings_ in the left-hand sidebar, and then click _Authentication security_. On this page, check _Enable SCIM configuration_ and press _Save_. After that, you should see a message reading _SCIM Enabled_.
|
||||
|
||||
Before we create a SCIM provider, we have to create a new Property Mapping. In authentik, go to _Customization_, then _Property Mappings_. Here, click _Create_, select _SCIM Provider Mapping_. Name the mapping something memorable and paste the following code in the _Expression_ field:
|
||||
|
||||
```python
|
||||
entitlement_names = {
|
||||
entitlement.name
|
||||
for entitlement in request.user.app_entitlements(provider.application)
|
||||
}
|
||||
|
||||
roles = []
|
||||
# Edit this if statement if you need to add more GitHub roles.
|
||||
# Valid roles include:
|
||||
# user, guest_collaborator, enterprise_owner, billing_manager
|
||||
if "GitHub Admins" in entitlement_names:
|
||||
roles.append({'value': 'enterprise_owner', 'primary': True})
|
||||
elif "GitHub Users" in entitlement_names:
|
||||
roles.append({'value': 'user', 'primary': True})
|
||||
|
||||
return {
|
||||
"roles": roles,
|
||||
}
|
||||
```
|
||||
|
||||
If you renamed either entitlement, make sure that you update the code above to match.
|
||||
|
||||
Create a new SCIM provider with the following parameters:
|
||||
|
||||
- URL: `https://github.company/api/v3/scim/v2`
|
||||
- Token: Paste the token you generated earlier here.
|
||||
- In the _Attribute mapping_ section, de-select the `authentik default SCIM Mapping: User` mapping from the _User Property Mappings_ by selecting it on the right-hand side and clicking the left-facing single chevron.
|
||||
- Select the property mapping you created in the previous step and add it by clicking the right-facing single chevron.
|
||||
- Ensure that `authentik default SCIM Mapping: Group` is the only one active in the _Group Property Mappings_.
|
||||
- Click _Finish_.
|
||||
|
||||
Go back to your GitHub Enterprise Server Application created in the first step and add your new SCIM provider in the _Backchannel Providers_ field, then click the _Update_ button.
|
||||
|
||||
You should now be ready to assign users or groups to your _GitHub Users_ and _GitHub Admins_ application entitlements. Use application bindings or policies to limit which users can view the application and are synchronized by SCIM, and use the entitlements to assign the corresponding GitHub SCIM role values. If you do not see your users being provisioned, go to your SCIM provider and click the _Run sync again_ option. A few seconds later, you should see results of the SCIM sync.
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
title: Integrate with GitHub Organization
|
||||
sidebar_label: GitHub Organization
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is a GitHub Organization
|
||||
|
||||
> Organizations are shared accounts where businesses and open-source projects can collaborate across many projects at once, with sophisticated security and administrative features.
|
||||
>
|
||||
> -- https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `github.com/orgs/foo` is your GitHub organization, where `foo` is the name of your GitHub organization.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::info
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of GitHub Organization with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **New Application** to open the application wizard.
|
||||
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings. Take note of the **slug** as it will be required later.
|
||||
- **Choose a Provider type**: select **SAML Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Set the **ACS URL** to `https://github.com/orgs/foo/saml/consume`.
|
||||
- Set the **Audience** to `https://github.com/orgs/foo`.
|
||||
- Set the **Issuer** to `https://github.com/orgs/foo`.
|
||||
- Set the **Service Provider Binding** to `Post`.
|
||||
- Under **Advanced protocol settings**, select an available **Signing certificate**. It is advised to download this certificate as it will be required later. It can be found under **System** > **Certificates** in the Admin Interface.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/bindings-overview/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
Navigate to your organization settings by going to your organization page at https://github.com/foo, then click Settings.
|
||||
|
||||
In the left-hand navigation, scroll down to the Security section and click `Authentication security`.
|
||||
|
||||
On this page:
|
||||
|
||||
- Select the `Enable SAML authentication` checkbox.
|
||||
- In `sign-on URL`, type `https://authentik.company/application/saml/<application_slug>/sso/binding/redirect/`
|
||||
- For `Issuer`, type `https://github.com/orgs/foo` or the `Audience` you set in authentik
|
||||
- For `Public certificate`, paste the _full_ signing certificate into this field.
|
||||
- Verify that the `Signature method` and `Digest method` match your SAML provider settings in authentik.
|
||||
|
||||
Once these fields are populated, you can use the `Test SAML configuration` button to test the authentication flow. If the flow completes successfully, you will see a green tick next to the Test button.
|
||||
|
||||
Scroll down to hit the `Save` button below.
|
||||
|
||||

|
||||
|
||||
This enables SAML as an authentication _option_. If you want to _require_ SAML for your organization, visit your SSO url at `https://github.com/orgs/foo/sso` and sign in. Once signed in, you can navigate back to the `Authentication security` page and check `Require SAML SSO authentication for all members of the foo organization.`
|
||||
@@ -10,4 +10,18 @@
|
||||
/integrations/* /:splat 301!
|
||||
#endregion
|
||||
|
||||
#region GitHub integration renames
|
||||
/development/github-enterprise-cloud /development/ghec 301!
|
||||
/development/github-enterprise-cloud/ /development/ghec/ 301!
|
||||
|
||||
/development/github-organization /development/ghec 301!
|
||||
/development/github-organization/ /development/ghec/ 301!
|
||||
|
||||
/development/github-enterprise-emu /development/ghec-emu 301!
|
||||
/development/github-enterprise-emu/ /development/ghec-emu/ 301!
|
||||
|
||||
/development/github-enterprise-server /development/ghes 301!
|
||||
/development/github-enterprise-server/ /development/ghes/ 301!
|
||||
#endregion
|
||||
|
||||
/networking/cloudflare-access /security/cloudflare-access 301!
|
||||
|
||||