Compare commits

...
4 Commits
Author SHA1 Message Date
zemion aa91063211 Release Core v0.1.34 with JMAP mail contracts
Module Package Release / publish-packages (push) Successful in 14s
2026-08-22 17:08:54 +02:00
zemion fa2d5d40dd Release Core v0.1.33 with redirect-sensitive headers
Module Package Release / publish-packages (push) Successful in 13s
2026-08-22 16:11:02 +02:00
zemion 6ccef162f6 Release Core v0.1.32 with bounded HTTP request bodies
Module Package Release / publish-packages (push) Successful in 14s
2026-08-22 14:32:24 +02:00
zemion 48dac139a5 feat(wiki): compose governed Wiki WebUI
Module Package Release / publish-packages (push) Successful in 12s
2026-08-22 13:32:20 +02:00
11 changed files with 208 additions and 24 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-core" name = "govoplan-core"
version = "0.1.30" version = "0.1.34"
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components." description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+44 -5
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Mapping from typing import Mapping
@@ -12,6 +13,12 @@ from govoplan_core.security.outbound_http import (
) )
MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES = 1_000_000
_STANDARD_REDIRECT_SENSITIVE_HEADERS = frozenset(
{"authorization", "proxy-authorization", "cookie", "cookie2"}
)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class HttpFetchResponse: class HttpFetchResponse:
status: int status: int
@@ -46,15 +53,27 @@ def fetch_http(
label: str = "URL", label: str = "URL",
method: str = "GET", method: str = "GET",
headers: Mapping[str, str] | None = None, headers: Mapping[str, str] | None = None,
body: bytes | None = None,
max_bytes: int | None = None, max_bytes: int | None = None,
redirect_sensitive_headers: Iterable[str] = (),
) -> HttpFetchResponse: ) -> HttpFetchResponse:
if body is not None and len(body) > MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES:
raise ValueError(
"Outbound HTTP request body exceeds the 1000000-byte safety limit."
)
validated_url = validate_outbound_http_url(url, label=label) validated_url = validate_outbound_http_url(url, label=label)
request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S). request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S).
validated_url, validated_url,
data=body,
headers=dict(headers or {}), headers=dict(headers or {}),
method=method, method=method,
) )
opener = build_outbound_http_opener(_PolicyRedirectHandler(label=label)) opener = build_outbound_http_opener(
_PolicyRedirectHandler(
label=label,
sensitive_headers=redirect_sensitive_headers,
)
)
with opener.open(request, timeout=timeout) as response: # noqa: S310 - URL and every redirect are policy-validated. # nosec B310 # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected with opener.open(request, timeout=timeout) as response: # noqa: S310 - URL and every redirect are policy-validated. # nosec B310 # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
response_headers = dict(response.headers.items()) response_headers = dict(response.headers.items())
return HttpFetchResponse( return HttpFetchResponse(
@@ -76,16 +95,35 @@ def fetch_http_text(
label: str = "URL", label: str = "URL",
method: str = "GET", method: str = "GET",
headers: Mapping[str, str] | None = None, headers: Mapping[str, str] | None = None,
body: bytes | None = None,
encoding: str = "utf-8", encoding: str = "utf-8",
max_bytes: int | None = None, max_bytes: int | None = None,
redirect_sensitive_headers: Iterable[str] = (),
) -> str: ) -> str:
return fetch_http(url, timeout=timeout, label=label, method=method, headers=headers, max_bytes=max_bytes).text(encoding) return fetch_http(
url,
timeout=timeout,
label=label,
method=method,
headers=headers,
body=body,
max_bytes=max_bytes,
redirect_sensitive_headers=redirect_sensitive_headers,
).text(encoding)
class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler): class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
def __init__(self, *, label: str) -> None: def __init__(
self,
*,
label: str,
sensitive_headers: Iterable[str] = (),
) -> None:
super().__init__() super().__init__()
self._label = label self._label = label
self._sensitive_headers = _STANDARD_REDIRECT_SENSITIVE_HEADERS | {
value.strip().lower() for value in sensitive_headers if value.strip()
}
def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def] def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
candidate = validate_outbound_http_url(newurl, label=f"{self._label} redirect") candidate = validate_outbound_http_url(newurl, label=f"{self._label} redirect")
@@ -95,8 +133,9 @@ class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
return None return None
new_request = super().redirect_request(req, fp, code, msg, headers, candidate) new_request = super().redirect_request(req, fp, code, msg, headers, candidate)
if new_request is not None and _http_origin(previous) != _http_origin(redirected): if new_request is not None and _http_origin(previous) != _http_origin(redirected):
for header in ("Authorization", "Proxy-Authorization", "Cookie", "Cookie2"): for header in tuple(new_request.headers) + tuple(new_request.unredirected_hdrs):
new_request.remove_header(header) if header.lower() in self._sensitive_headers:
new_request.remove_header(header)
return new_request return new_request
+91 -4
View File
@@ -2,9 +2,14 @@ from __future__ import annotations
import io import io
import unittest import unittest
from unittest.mock import patch from unittest.mock import Mock, patch
from govoplan_core.security.http_fetch import _PolicyRedirectHandler, is_http_url, validate_http_url from govoplan_core.security.http_fetch import (
_PolicyRedirectHandler,
fetch_http,
is_http_url,
validate_http_url,
)
from govoplan_core.security.outbound_http import ( from govoplan_core.security.outbound_http import (
DEFAULT_FILE_TRANSFER_BYTES, DEFAULT_FILE_TRANSFER_BYTES,
DEFAULT_STRUCTURED_RESPONSE_BYTES, DEFAULT_STRUCTURED_RESPONSE_BYTES,
@@ -21,6 +26,51 @@ from govoplan_core.security.outbound_http import (
class HttpFetchTests(unittest.TestCase): class HttpFetchTests(unittest.TestCase):
def test_fetch_http_forwards_a_bounded_request_body(self) -> None:
class Response(io.BytesIO):
status = 200
headers = {"Content-Type": "application/json"}
def __enter__(self):
return self
def __exit__(self, *_args):
return False
opener = Mock()
opener.open.return_value = Response(b"{}")
with patch(
"govoplan_core.security.http_fetch.validate_outbound_http_url",
return_value="https://wiki.example.test/api.php",
), patch(
"govoplan_core.security.http_fetch.build_outbound_http_opener",
return_value=opener,
):
response = fetch_http(
"https://wiki.example.test/api.php",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body=b"action=edit",
max_bytes=1024,
)
request = opener.open.call_args.args[0]
self.assertEqual("POST", request.get_method())
self.assertEqual(b"action=edit", request.data)
self.assertEqual(b"{}", response.body)
def test_fetch_http_rejects_an_oversized_request_body_before_transport(self) -> None:
with patch(
"govoplan_core.security.http_fetch.validate_outbound_http_url"
) as validate:
with self.assertRaisesRegex(ValueError, "request body exceeds"):
fetch_http(
"https://wiki.example.test/api.php",
method="POST",
body=b"x" * 1_000_001,
)
validate.assert_not_called()
def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None: def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None:
self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json")) self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json"))
self.assertTrue(is_http_url("http://example.test/catalog.json")) self.assertTrue(is_http_url("http://example.test/catalog.json"))
@@ -189,9 +239,17 @@ class HttpFetchTests(unittest.TestCase):
request = urllib.request.Request( request = urllib.request.Request(
"https://catalog.example.test/releases", "https://catalog.example.test/releases",
headers={"Authorization": "Bearer secret", "X-Request-ID": "request-1"}, headers={
"Authorization": "Bearer secret",
"Cookie": "session=secret",
"X-OTRS-Header-Password": "secret",
"X-Request-ID": "request-1",
},
)
handler = _PolicyRedirectHandler(
label="Catalog URL",
sensitive_headers=("X-OTRS-Header-Password",),
) )
handler = _PolicyRedirectHandler(label="Catalog URL")
with patch.dict("os.environ", {"APP_ENV": "test"}), patch( with patch.dict("os.environ", {"APP_ENV": "test"}), patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo", "govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("127.0.0.1", 443))], return_value=[(2, 1, 6, "", ("127.0.0.1", 443))],
@@ -215,9 +273,38 @@ class HttpFetchTests(unittest.TestCase):
self.assertIsNotNone(redirected) self.assertIsNotNone(redirected)
self.assertIsNone(redirected.get_header("Authorization")) self.assertIsNone(redirected.get_header("Authorization"))
self.assertIsNone(redirected.get_header("Cookie"))
self.assertIsNone(redirected.get_header("X-otrs-header-password"))
self.assertEqual("request-1", redirected.get_header("X-request-id")) self.assertEqual("request-1", redirected.get_header("X-request-id"))
self.assertIsNone(downgrade) self.assertIsNone(downgrade)
def test_core_redirects_preserve_caller_sensitive_headers_on_the_same_origin(self) -> None:
import urllib.request
request = urllib.request.Request(
"https://desk.example.test/original",
headers={"X-OTRS-Header-SessionID": "secret"},
)
handler = _PolicyRedirectHandler(
label="Service-desk URL",
sensitive_headers=("X-OTRS-Header-SessionID",),
)
with patch.dict("os.environ", {"APP_ENV": "test"}), patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("127.0.0.1", 443))],
):
redirected = handler.redirect_request(
request,
None,
302,
"Found",
{},
"https://desk.example.test/final",
)
self.assertIsNotNone(redirected)
self.assertEqual("secret", redirected.get_header("X-otrs-header-sessionid"))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+25 -4
View File
@@ -1,12 +1,12 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.30", "version": "0.1.34",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.30", "version": "0.1.34",
"dependencies": { "dependencies": {
"@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/access-webui": "file:../../govoplan-access/webui",
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
@@ -52,6 +52,7 @@
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui", "@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui",
"@govoplan/voting-webui": "file:../../govoplan-voting/webui", "@govoplan/voting-webui": "file:../../govoplan-voting/webui",
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
"@tiptap/core": "^3.29.2", "@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2", "@tiptap/extension-image": "^3.29.2",
@@ -242,7 +243,7 @@
}, },
"../../govoplan-connectors/webui": { "../../govoplan-connectors/webui": {
"name": "@govoplan/connectors-webui", "name": "@govoplan/connectors-webui",
"version": "0.1.20", "version": "0.1.22",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"react": ">=19.2.7 <20", "react": ">=19.2.7 <20",
@@ -474,7 +475,7 @@
}, },
"../../govoplan-mail/webui": { "../../govoplan-mail/webui": {
"name": "@govoplan/mail-webui", "name": "@govoplan/mail-webui",
"version": "0.1.21", "version": "0.1.22",
"devDependencies": { "devDependencies": {
"typescript": "^5.7.2" "typescript": "^5.7.2"
}, },
@@ -822,6 +823,22 @@
} }
} }
}, },
"../../govoplan-wiki/webui": {
"name": "@govoplan/wiki-webui",
"version": "0.1.20",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.31",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
},
"../../govoplan-workflow/webui": { "../../govoplan-workflow/webui": {
"name": "@govoplan/workflow-webui", "name": "@govoplan/workflow-webui",
"version": "0.1.21", "version": "0.1.21",
@@ -1768,6 +1785,10 @@
"resolved": "../../govoplan-voting/webui", "resolved": "../../govoplan-voting/webui",
"link": true "link": true
}, },
"node_modules/@govoplan/wiki-webui": {
"resolved": "../../govoplan-wiki/webui",
"link": true
},
"node_modules/@govoplan/workflow-webui": { "node_modules/@govoplan/workflow-webui": {
"resolved": "../../govoplan-workflow/webui", "resolved": "../../govoplan-workflow/webui",
"link": true "link": true
+19 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.30", "version": "0.1.34",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.30", "version": "0.1.34",
"dependencies": { "dependencies": {
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19", "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18", "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
@@ -24,6 +24,7 @@
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18", "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18", "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20", "@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
"@tiptap/core": "^3.29.2", "@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2", "@tiptap/extension-image": "^3.29.2",
"@tiptap/pm": "^3.29.2", "@tiptap/pm": "^3.29.2",
@@ -1032,6 +1033,22 @@
} }
} }
}, },
"node_modules/@govoplan/wiki-webui": {
"version": "0.1.20",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#66c91351c9eb693ace606c9b69dd5cd804d7531b",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.31",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.30", "version": "0.1.34",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -102,6 +102,7 @@
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui", "@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui",
"@govoplan/voting-webui": "file:../../govoplan-voting/webui", "@govoplan/voting-webui": "file:../../govoplan-voting/webui",
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
"@tiptap/core": "^3.29.2", "@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2", "@tiptap/extension-image": "^3.29.2",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.30", "version": "0.1.34",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -42,6 +42,7 @@
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18", "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18", "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20", "@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
"@tiptap/core": "^3.29.2", "@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2", "@tiptap/extension-image": "^3.29.2",
"@tiptap/pm": "^3.29.2", "@tiptap/pm": "^3.29.2",
+4 -1
View File
@@ -46,6 +46,7 @@ const packageByModule = {
tickets: "@govoplan/tickets-webui", tickets: "@govoplan/tickets-webui",
views: "@govoplan/views-webui", views: "@govoplan/views-webui",
voting: "@govoplan/voting-webui", voting: "@govoplan/voting-webui",
wiki: "@govoplan/wiki-webui",
workflow: "@govoplan/workflow-webui" workflow: "@govoplan/workflow-webui"
}; };
@@ -79,6 +80,8 @@ const cases = [
{ name: "forms-runtime", modules: ["forms", "forms_runtime"] }, { name: "forms-runtime", modules: ["forms", "forms_runtime"] },
{ name: "tickets-only", modules: ["tickets"] }, { name: "tickets-only", modules: ["tickets"] },
{ name: "tickets-with-helpdesk-and-cases", modules: ["tickets", "helpdesk", "cases"] }, { name: "tickets-with-helpdesk-and-cases", modules: ["tickets", "helpdesk", "cases"] },
{ name: "wiki-only", modules: ["wiki"] },
{ name: "wiki-with-files-search", modules: ["wiki", "files", "search"] },
{ name: "mail-only", modules: ["mail"] }, { name: "mail-only", modules: ["mail"] },
{ name: "notifications-only", modules: ["notifications"] }, { name: "notifications-only", modules: ["notifications"] },
{ name: "organizations-only", modules: ["organizations"] }, { name: "organizations-only", modules: ["organizations"] },
@@ -110,7 +113,7 @@ const cases = [
{ name: "tasks-only", modules: ["access", "tasks"] }, { name: "tasks-only", modules: ["access", "tasks"] },
{ name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] }, { name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] },
{ name: "voting-only", modules: ["access", "voting"] }, { name: "voting-only", modules: ["access", "voting"] },
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting"] } { name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting", "wiki"] }
]; ];
const npmExec = process.env.npm_execpath; const npmExec = process.env.npm_execpath;
+5 -2
View File
@@ -35,7 +35,7 @@ export type MailProfilePatternRules = Partial<Record<MailProfilePatternKey, stri
export type MailConnectionTestResponse = { export type MailConnectionTestResponse = {
ok: boolean; ok: boolean;
protocol: "smtp" | "imap"; protocol: "smtp" | "imap" | "jmap" | "pop3";
host?: string | null; host?: string | null;
port?: number | null; port?: number | null;
security?: MailSecurity | string | null; security?: MailSecurity | string | null;
@@ -52,7 +52,7 @@ export type MailImapFolderResponse = {
export type MailImapFolderListResponse = { export type MailImapFolderListResponse = {
ok: boolean; ok: boolean;
protocol: "imap"; protocol: "imap" | "jmap";
host?: string | null; host?: string | null;
port?: number | null; port?: number | null;
security?: MailSecurity | string | null; security?: MailSecurity | string | null;
@@ -69,6 +69,7 @@ export type MailImapFolderListResponse = {
export const mailProfilePatternKeys = [ export const mailProfilePatternKeys = [
"smtp_hosts", "smtp_hosts",
"imap_hosts", "imap_hosts",
"jmap_hosts",
"envelope_senders", "envelope_senders",
"from_headers", "from_headers",
"recipient_domains" "recipient_domains"
@@ -82,11 +83,13 @@ export const mailProfilePolicyLimitKeys = [
"imap_credentials.inherit", "imap_credentials.inherit",
"whitelist.smtp_hosts", "whitelist.smtp_hosts",
"whitelist.imap_hosts", "whitelist.imap_hosts",
"whitelist.jmap_hosts",
"whitelist.envelope_senders", "whitelist.envelope_senders",
"whitelist.from_headers", "whitelist.from_headers",
"whitelist.recipient_domains", "whitelist.recipient_domains",
"blacklist.smtp_hosts", "blacklist.smtp_hosts",
"blacklist.imap_hosts", "blacklist.imap_hosts",
"blacklist.jmap_hosts",
"blacklist.envelope_senders", "blacklist.envelope_senders",
"blacklist.from_headers", "blacklist.from_headers",
"blacklist.recipient_domains" "blacklist.recipient_domains"
+13 -3
View File
@@ -847,6 +847,16 @@ export type MailImapTransportSettings = MailTransportSettings & {
folder_mappings?: MailImapFolderMappings | null; folder_mappings?: MailImapFolderMappings | null;
}; };
export type MailJmapTransportSettings = {
session_url: string;
account_id?: string | null;
auth_scheme?: "bearer" | "basic";
timeout_seconds?: number | null;
max_response_bytes?: number | null;
max_body_value_bytes?: number | null;
allowed_api_origins?: string[];
};
export type MailServerProfileCredentials = { export type MailServerProfileCredentials = {
smtp?: MailTransportCredentials | null; smtp?: MailTransportCredentials | null;
imap?: MailTransportCredentials | null; imap?: MailTransportCredentials | null;
@@ -883,9 +893,9 @@ export type MailServerEndpoint = {
id: string; id: string;
profile_id: string; profile_id: string;
tenant_id?: string | null; tenant_id?: string | null;
protocol: "smtp" | "imap"; protocol: "smtp" | "imap" | "jmap" | "pop3";
name: string; name: string;
config: MailTransportSettings | MailImapTransportSettings; config: MailTransportSettings | MailImapTransportSettings | MailJmapTransportSettings;
scope_type: MailProfileScope; scope_type: MailProfileScope;
scope_id?: string | null; scope_id?: string | null;
inherit_to_lower_scopes: boolean; inherit_to_lower_scopes: boolean;
@@ -922,7 +932,7 @@ export type MailCredentialPolicy = {
allow_override?: boolean | null; allow_override?: boolean | null;
}; };
export type MailProfilePatternKey = "smtp_hosts" | "imap_hosts" | "envelope_senders" | "from_headers" | "recipient_domains"; export type MailProfilePatternKey = "smtp_hosts" | "imap_hosts" | "jmap_hosts" | "envelope_senders" | "from_headers" | "recipient_domains";
export type MailProfilePolicy = { export type MailProfilePolicy = {
allowed_profile_ids?: string[] | null; allowed_profile_ids?: string[] | null;
+2
View File
@@ -56,6 +56,7 @@ const defaultWebModulePackages = [
"@govoplan/tickets-webui", "@govoplan/tickets-webui",
"@govoplan/views-webui", "@govoplan/views-webui",
"@govoplan/voting-webui", "@govoplan/voting-webui",
"@govoplan/wiki-webui",
"@govoplan/workflow-webui" "@govoplan/workflow-webui"
]; ];
@@ -289,6 +290,7 @@ export default defineConfig({
fileURLToPath(new URL('../../govoplan-tickets/webui', import.meta.url)), fileURLToPath(new URL('../../govoplan-tickets/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)), fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)), fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-wiki/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url)) fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
] ]
}, },