mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Import app route manifests
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
> Status: draft | Updated 2026-07-05 | Applies to: AMPG contributors and operators
|
> Status: draft | Updated 2026-07-05 | Applies to: AMPG contributors and operators
|
||||||
|
|
||||||
Adaptive Multi-Protocol Gateway, or AMPG, turns one canonical site into transport-appropriate
|
Adaptive Multi-Protocol Gateway, or AMPG, turns one canonical site into transport-appropriate
|
||||||
published versions for clearnet web, Tor onion services, I2P, Gemini, and Reticulum.
|
published versions for clearnet web, Tor onion services, I2P, Gemini, IPFS, and
|
||||||
|
Reticulum.
|
||||||
|
|
||||||
The first use case is practical: start from an existing clearnet static site such as
|
The first use case is practical: start from an existing clearnet static site such as
|
||||||
`wownero.org`, keep the rich web version intact, and generate privacy-hardened or
|
`wownero.org`, keep the rich web version intact, and generate privacy-hardened or
|
||||||
@@ -64,14 +65,16 @@ AMPG is static-first, but not static-only. Each site or route group declares the
|
|||||||
interaction tier it wants AMPG to expose:
|
interaction tier it wants AMPG to expose:
|
||||||
|
|
||||||
- `static`: rendered files only.
|
- `static`: rendered files only.
|
||||||
- `forms`: server-rendered GET/POST actions with full-page responses.
|
- `interactive-lite`: deterministic games, status lookups, quote forms, and other flows
|
||||||
- `sessions`: authenticated account flows with HTTP-only cookies or transport-native identity.
|
without account state or payment confirmation.
|
||||||
- `realtime`: rich browser/admin surfaces; HTTP transports only.
|
- `identity`: authenticated sessions, wallet sign-in, or signed capability links.
|
||||||
|
- `transactional`: server-confirmed orders, deposits, invoices, donations, or payment intents.
|
||||||
|
- `realtime`: multiplayer, dashboards, websockets, streaming updates, or fast shared state.
|
||||||
- `internal`: workers, webhooks, private admin APIs; never published automatically.
|
- `internal`: workers, webhooks, private admin APIs; never published automatically.
|
||||||
|
|
||||||
Constrained transports receive the safest compatible version. A storefront can have a Gemini
|
Constrained transports receive the safest compatible version. A storefront can have a
|
||||||
catalog and inquiry flow without exposing its full JavaScript checkout. A worker API remains
|
Gemini catalog and inquiry flow without exposing its full JavaScript checkout. A worker
|
||||||
internal even when the public site is available on Tor or I2P.
|
API remains internal even when the public site is available on Tor or I2P.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
+79
-16
@@ -1,11 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
|
|
||||||
|
ROUTE_MANIFEST_SCHEMA = "ampg.route-manifest.v1"
|
||||||
|
|
||||||
DEFAULT_DAEMONS = {
|
DEFAULT_DAEMONS = {
|
||||||
"clearnet": "nginx",
|
"clearnet": "nginx",
|
||||||
"tor": "tor",
|
"tor": "tor",
|
||||||
@@ -63,6 +66,7 @@ class InteractionConfig:
|
|||||||
default_tier: str = "static"
|
default_tier: str = "static"
|
||||||
deny_routes: tuple[str, ...] = ()
|
deny_routes: tuple[str, ...] = ()
|
||||||
routes: tuple[RoutePolicyConfig, ...] = ()
|
routes: tuple[RoutePolicyConfig, ...] = ()
|
||||||
|
route_manifest: Path | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -122,7 +126,7 @@ def _parse_site(raw_site: dict[str, Any], base_dir: Path) -> SiteConfig:
|
|||||||
),
|
),
|
||||||
outputs=OutputConfig(root=output_root, plan_root=plan_root),
|
outputs=OutputConfig(root=output_root, plan_root=plan_root),
|
||||||
protocols=parsed_protocols,
|
protocols=parsed_protocols,
|
||||||
interactions=_parse_interactions(interactions),
|
interactions=_parse_interactions(interactions, base_dir, site_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,28 +148,44 @@ def _parse_protocol(name: str, raw_protocol: dict[str, Any]) -> ProtocolConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_interactions(raw_interactions: dict[str, Any]) -> InteractionConfig:
|
def _parse_interactions(
|
||||||
raw_routes = raw_interactions.get("route", ())
|
raw_interactions: dict[str, Any],
|
||||||
if isinstance(raw_routes, dict):
|
base_dir: Path,
|
||||||
raw_routes = (raw_routes,)
|
site_id: str,
|
||||||
if not isinstance(raw_routes, (list, tuple)):
|
) -> InteractionConfig:
|
||||||
raise ValueError("site.interactions.route must be a table array")
|
if not isinstance(raw_interactions, dict):
|
||||||
deny_routes = raw_interactions.get("deny_routes", ())
|
raise ValueError("site.interactions must be a table")
|
||||||
if not isinstance(deny_routes, (list, tuple)):
|
|
||||||
raise ValueError("site.interactions.deny_routes must be an array")
|
route_manifest = _route_manifest_path(raw_interactions, base_dir)
|
||||||
|
manifest_data = _load_route_manifest(route_manifest, site_id) if route_manifest else {}
|
||||||
|
|
||||||
|
default_tier = str(
|
||||||
|
raw_interactions.get("default_tier", manifest_data.get("default_tier", "static"))
|
||||||
|
)
|
||||||
|
deny_routes = _string_array(manifest_data.get("deny_routes", ()), "route manifest deny_routes")
|
||||||
|
deny_routes += _string_array(raw_interactions.get("deny_routes", ()), "site.interactions.deny_routes")
|
||||||
|
routes = tuple(
|
||||||
|
_parse_route_policy(raw_route, default_tier)
|
||||||
|
for raw_route in _route_array(manifest_data.get("routes", ()), "route manifest routes")
|
||||||
|
)
|
||||||
|
routes += tuple(
|
||||||
|
_parse_route_policy(raw_route, default_tier)
|
||||||
|
for raw_route in _route_array(raw_interactions.get("route", ()), "site.interactions.route")
|
||||||
|
)
|
||||||
return InteractionConfig(
|
return InteractionConfig(
|
||||||
default_tier=str(raw_interactions.get("default_tier", "static")),
|
default_tier=default_tier,
|
||||||
deny_routes=tuple(str(route) for route in deny_routes),
|
deny_routes=tuple(deny_routes),
|
||||||
routes=tuple(_parse_route_policy(raw_route) for raw_route in raw_routes),
|
routes=routes,
|
||||||
|
route_manifest=route_manifest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_route_policy(raw_route: dict[str, Any]) -> RoutePolicyConfig:
|
def _parse_route_policy(raw_route: dict[str, Any], default_tier: str) -> RoutePolicyConfig:
|
||||||
if not isinstance(raw_route, dict):
|
if not isinstance(raw_route, dict):
|
||||||
raise ValueError("site.interactions.route entries must be tables")
|
raise ValueError("route policy entries must be objects")
|
||||||
return RoutePolicyConfig(
|
return RoutePolicyConfig(
|
||||||
match=_required_str(raw_route, "match"),
|
match=_required_str(raw_route, "match"),
|
||||||
tier=str(raw_route.get("tier", "static")),
|
tier=str(raw_route.get("tier", default_tier)),
|
||||||
identity=str(raw_route.get("identity", "none")),
|
identity=str(raw_route.get("identity", "none")),
|
||||||
payments=str(raw_route.get("payments", "none")),
|
payments=str(raw_route.get("payments", "none")),
|
||||||
realtime=bool(raw_route.get("realtime", False)),
|
realtime=bool(raw_route.get("realtime", False)),
|
||||||
@@ -173,6 +193,49 @@ def _parse_route_policy(raw_route: dict[str, Any]) -> RoutePolicyConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _route_manifest_path(raw_interactions: dict[str, Any], base_dir: Path) -> Path | None:
|
||||||
|
raw_path = raw_interactions.get("route_manifest")
|
||||||
|
if raw_path is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(raw_path, str) or not raw_path:
|
||||||
|
raise ValueError("site.interactions.route_manifest must be a path string")
|
||||||
|
return _resolve_path(base_dir, raw_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_route_manifest(path: Path, site_id: str) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise ValueError(f"{site_id}: route manifest not found: {path}") from exc
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"{site_id}: invalid route manifest JSON: {path}: {exc}") from exc
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(f"{site_id}: route manifest must be a JSON object: {path}")
|
||||||
|
if data.get("schema") != ROUTE_MANIFEST_SCHEMA:
|
||||||
|
raise ValueError(
|
||||||
|
f"{site_id}: route manifest schema must be {ROUTE_MANIFEST_SCHEMA!r}: {path}"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _route_array(raw_routes: Any, context: str) -> tuple[Any, ...]:
|
||||||
|
if isinstance(raw_routes, dict):
|
||||||
|
return (raw_routes,)
|
||||||
|
if raw_routes is None:
|
||||||
|
return ()
|
||||||
|
if not isinstance(raw_routes, (list, tuple)):
|
||||||
|
raise ValueError(f"{context} must be an array")
|
||||||
|
return tuple(raw_routes)
|
||||||
|
|
||||||
|
|
||||||
|
def _string_array(raw_values: Any, context: str) -> tuple[str, ...]:
|
||||||
|
if raw_values is None:
|
||||||
|
return ()
|
||||||
|
if not isinstance(raw_values, (list, tuple)):
|
||||||
|
raise ValueError(f"{context} must be an array")
|
||||||
|
return tuple(str(value) for value in raw_values)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_path(base_dir: Path, raw_path: str) -> Path:
|
def _resolve_path(base_dir: Path, raw_path: str) -> Path:
|
||||||
path = Path(raw_path).expanduser()
|
path = Path(raw_path).expanduser()
|
||||||
if not path.is_absolute():
|
if not path.is_absolute():
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ CONFIG_FIELDS = (
|
|||||||
"[]",
|
"[]",
|
||||||
"Route patterns that must not be published.",
|
"Route patterns that must not be published.",
|
||||||
),
|
),
|
||||||
|
ConfigField(
|
||||||
|
"site.interactions",
|
||||||
|
"route_manifest",
|
||||||
|
"path",
|
||||||
|
False,
|
||||||
|
"",
|
||||||
|
"JSON route manifest generated or maintained by the application.",
|
||||||
|
),
|
||||||
ConfigField(
|
ConfigField(
|
||||||
"site.interactions.route",
|
"site.interactions.route",
|
||||||
"match",
|
"match",
|
||||||
@@ -123,6 +131,22 @@ CONFIG_FIELDS = (
|
|||||||
"none",
|
"none",
|
||||||
"Payment adapter required by a route group.",
|
"Payment adapter required by a route group.",
|
||||||
),
|
),
|
||||||
|
ConfigField(
|
||||||
|
"site.interactions.route",
|
||||||
|
"realtime",
|
||||||
|
"boolean",
|
||||||
|
False,
|
||||||
|
"false",
|
||||||
|
"Whether a route group requires live state or streaming updates.",
|
||||||
|
),
|
||||||
|
ConfigField(
|
||||||
|
"site.interactions.route",
|
||||||
|
"public_allowed",
|
||||||
|
"boolean",
|
||||||
|
False,
|
||||||
|
"true",
|
||||||
|
"Whether this route group may be emitted into public protocol outputs.",
|
||||||
|
),
|
||||||
ConfigField(
|
ConfigField(
|
||||||
"site.protocols.<name>",
|
"site.protocols.<name>",
|
||||||
"enabled",
|
"enabled",
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ openapi = "http://127.0.0.1:9000/openapi.json"
|
|||||||
|
|
||||||
[site.interactions]
|
[site.interactions]
|
||||||
default_tier = "interactive-lite"
|
default_tier = "interactive-lite"
|
||||||
|
route_manifest = "./routes.json"
|
||||||
session_policy = "http-only-cookie"
|
session_policy = "http-only-cookie"
|
||||||
deny_routes = ["/admin/*", "/api/internal/*", "/webhooks/*"]
|
deny_routes = ["/admin/*", "/api/internal/*", "/webhooks/*"]
|
||||||
|
|
||||||
@@ -196,6 +197,38 @@ max_tier = "interactive-lite"
|
|||||||
`max_tier` is a hard cap. If a route needs a higher tier than the protocol allows, AMPG
|
`max_tier` is a hard cap. If a route needs a higher tier than the protocol allows, AMPG
|
||||||
omits it or renders a safe alternate page.
|
omits it or renders a safe alternate page.
|
||||||
|
|
||||||
|
## Route manifests
|
||||||
|
|
||||||
|
Applications can generate a JSON route manifest instead of hand-maintaining every route
|
||||||
|
group in TOML. Paths are resolved relative to `gateway.toml`; inline TOML routes are
|
||||||
|
appended after manifest routes.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "ampg.route-manifest.v1",
|
||||||
|
"default_tier": "interactive-lite",
|
||||||
|
"deny_routes": ["/admin/*", "/api/internal/*", "/webhooks/*"],
|
||||||
|
"routes": [
|
||||||
|
{"match": "/play/*"},
|
||||||
|
{
|
||||||
|
"match": "/checkout/*",
|
||||||
|
"tier": "transactional",
|
||||||
|
"identity": "http-session",
|
||||||
|
"payments": "server-invoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "/webhooks/*",
|
||||||
|
"tier": "internal",
|
||||||
|
"public_allowed": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Route entries use the same fields as `[[site.interactions.route]]`. The manifest should
|
||||||
|
describe public intent only; credentials, keys, deployment notes, and private hostnames do
|
||||||
|
not belong in it.
|
||||||
|
|
||||||
## Protocol-only deployments
|
## Protocol-only deployments
|
||||||
|
|
||||||
Enabled protocols are independent. A config can omit clearnet entirely and publish only
|
Enabled protocols are independent. A config can omit clearnet entirely and publish only
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ Fixture interaction policy defaults to:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Route-group overrides will be added separately. Until then, protocol-level options can
|
Protocol-level options can declare the base fixture policy that AMPB should check.
|
||||||
declare the fixture-level policy that AMPB should check.
|
|
||||||
|
|
||||||
When `[[site.interactions.route]]` entries are configured, AMPG emits additional fixtures
|
When `[[site.interactions.route]]` entries are configured, AMPG emits additional fixtures
|
||||||
for public route groups. Routes with `tier = "internal"` or `public_allowed = false` are
|
for public route groups. Routes with `tier = "internal"` or `public_allowed = false` are
|
||||||
not emitted.
|
not emitted.
|
||||||
|
|
||||||
|
Routes imported from `site.interactions.route_manifest` are treated the same way as inline
|
||||||
|
TOML routes.
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ This file is generated from code. Do not edit it by hand.
|
|||||||
| site.outputs | `plan_root` | path | no | ../dist/ampg-plan | Generated plan artifact root. |
|
| site.outputs | `plan_root` | path | no | ../dist/ampg-plan | Generated plan artifact root. |
|
||||||
| site.interactions | `default_tier` | enum | no | static | Default interaction tier for routes. |
|
| site.interactions | `default_tier` | enum | no | static | Default interaction tier for routes. |
|
||||||
| site.interactions | `deny_routes` | array<string> | no | [] | Route patterns that must not be published. |
|
| site.interactions | `deny_routes` | array<string> | no | [] | Route patterns that must not be published. |
|
||||||
|
| site.interactions | `route_manifest` | path | no | - | JSON route manifest generated or maintained by the application. |
|
||||||
| site.interactions.route | `match` | glob | no | - | Route pattern for an explicit interaction policy. |
|
| site.interactions.route | `match` | glob | no | - | Route pattern for an explicit interaction policy. |
|
||||||
| site.interactions.route | `tier` | enum | no | site.interactions.default_tier | Interaction tier for a route group. |
|
| site.interactions.route | `tier` | enum | no | site.interactions.default_tier | Interaction tier for a route group. |
|
||||||
| site.interactions.route | `identity` | enum | no | none | Identity adapter required by a route group. |
|
| site.interactions.route | `identity` | enum | no | none | Identity adapter required by a route group. |
|
||||||
| site.interactions.route | `payments` | enum | no | none | Payment adapter required by a route group. |
|
| site.interactions.route | `payments` | enum | no | none | Payment adapter required by a route group. |
|
||||||
|
| site.interactions.route | `realtime` | boolean | no | false | Whether a route group requires live state or streaming updates. |
|
||||||
|
| site.interactions.route | `public_allowed` | boolean | no | true | Whether this route group may be emitted into public protocol outputs. |
|
||||||
| site.protocols.<name> | `enabled` | boolean | no | false | Whether this protocol target is built and planned. |
|
| site.protocols.<name> | `enabled` | boolean | no | false | Whether this protocol target is built and planned. |
|
||||||
| site.protocols.<name> | `renderer` | enum | no | <protocol> | Renderer profile to use for this protocol. |
|
| site.protocols.<name> | `renderer` | enum | no | <protocol> | Renderer profile to use for this protocol. |
|
||||||
| site.protocols.<name> | `daemon` | enum | no | protocol default | Ingress daemon adapter. |
|
| site.protocols.<name> | `daemon` | enum | no | protocol default | Ingress daemon adapter. |
|
||||||
|
|||||||
@@ -49,6 +49,23 @@ Configured public route groups are emitted into fixture manifests so AMPB can ve
|
|||||||
route and interaction policy for each selected transport. Internal route groups are not
|
route and interaction policy for each selected transport. Internal route groups are not
|
||||||
emitted.
|
emitted.
|
||||||
|
|
||||||
|
## Route Manifest Contract
|
||||||
|
|
||||||
|
Apps may provide `ampg.route-manifest.v1` JSON. AMPG imports it from
|
||||||
|
`site.interactions.route_manifest`, merges its denylist with inline TOML, and appends any
|
||||||
|
inline TOML route entries after the imported route entries.
|
||||||
|
|
||||||
|
Required top-level fields:
|
||||||
|
|
||||||
|
- `schema`: must be `ampg.route-manifest.v1`.
|
||||||
|
- `routes`: route policy objects using `match`, `tier`, `identity`, `payments`,
|
||||||
|
`realtime`, and `public_allowed`.
|
||||||
|
|
||||||
|
Optional top-level fields:
|
||||||
|
|
||||||
|
- `default_tier`: inherited by route entries without a `tier`.
|
||||||
|
- `deny_routes`: route patterns that must not be published.
|
||||||
|
|
||||||
## Protocol Mapping
|
## Protocol Mapping
|
||||||
|
|
||||||
| Tier | Clearnet | Tor/I2P | Gemini | IPFS | Reticulum |
|
| Tier | Clearnet | Tor/I2P | Gemini | IPFS | Reticulum |
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"schema": "ampg.route-manifest.v1",
|
||||||
|
"default_tier": "interactive-lite",
|
||||||
|
"deny_routes": [
|
||||||
|
"/admin/*",
|
||||||
|
"/api/internal/*",
|
||||||
|
"/webhooks/*"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": "/play/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "/checkout/*",
|
||||||
|
"tier": "transactional",
|
||||||
|
"identity": "http-session",
|
||||||
|
"payments": "server-invoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "/live/*",
|
||||||
|
"tier": "realtime",
|
||||||
|
"identity": "http-session",
|
||||||
|
"realtime": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "/admin/*",
|
||||||
|
"tier": "internal",
|
||||||
|
"public_allowed": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -189,6 +189,120 @@ renderer = "privacy-html"
|
|||||||
self.assertEqual("transactional", by_path["/checkout/"]["interaction"]["tier"])
|
self.assertEqual("transactional", by_path["/checkout/"]["interaction"]["tier"])
|
||||||
self.assertEqual("server-invoice", by_path["/checkout/"]["interaction"]["payments"])
|
self.assertEqual("server-invoice", by_path["/checkout/"]["interaction"]["payments"])
|
||||||
|
|
||||||
|
def test_manifest_imports_app_route_manifest(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "site"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.html").write_text("<h1>App</h1>", encoding="utf-8")
|
||||||
|
(root / "routes.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema": "ampg.route-manifest.v1",
|
||||||
|
"default_tier": "interactive-lite",
|
||||||
|
"deny_routes": ["/admin/*"],
|
||||||
|
"routes": [
|
||||||
|
{"match": "/play/*"},
|
||||||
|
{
|
||||||
|
"match": "/checkout/*",
|
||||||
|
"tier": "transactional",
|
||||||
|
"identity": "http-session",
|
||||||
|
"payments": "server-invoice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "/webhooks/*",
|
||||||
|
"tier": "internal",
|
||||||
|
"public_allowed": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[[site]]
|
||||||
|
id = "app"
|
||||||
|
domain = "app.test"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "./site"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "./out"
|
||||||
|
plan_root = "./plan"
|
||||||
|
|
||||||
|
[site.interactions]
|
||||||
|
route_manifest = "./routes.json"
|
||||||
|
deny_routes = ["/api/internal/*"]
|
||||||
|
|
||||||
|
[[site.interactions.route]]
|
||||||
|
match = "/account/*"
|
||||||
|
tier = "identity"
|
||||||
|
identity = "http-session"
|
||||||
|
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
manifest = fixture_manifest(config.sites[0])
|
||||||
|
|
||||||
|
site = config.sites[0]
|
||||||
|
self.assertEqual("interactive-lite", site.interactions.default_tier)
|
||||||
|
self.assertEqual(("/admin/*", "/api/internal/*"), site.interactions.deny_routes)
|
||||||
|
self.assertEqual(4, len(site.interactions.routes))
|
||||||
|
self.assertEqual(4, len(manifest["fixtures"]))
|
||||||
|
|
||||||
|
by_path = {fixture.get("route", {}).get("fixture_path", "/"): fixture for fixture in manifest["fixtures"]}
|
||||||
|
self.assertEqual("interactive-lite", by_path["/play/"]["interaction"]["tier"])
|
||||||
|
self.assertEqual("http://app.i2p/play/", by_path["/play/"]["url"])
|
||||||
|
self.assertEqual("transactional", by_path["/checkout/"]["interaction"]["tier"])
|
||||||
|
self.assertEqual("server-invoice", by_path["/checkout/"]["interaction"]["payments"])
|
||||||
|
self.assertEqual("identity", by_path["/account/"]["interaction"]["tier"])
|
||||||
|
self.assertNotIn("/webhooks/", by_path)
|
||||||
|
|
||||||
|
def test_route_manifest_schema_is_required(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "site"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.html").write_text("<h1>App</h1>", encoding="utf-8")
|
||||||
|
(root / "routes.json").write_text('{"routes": []}', encoding="utf-8")
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[[site]]
|
||||||
|
id = "app"
|
||||||
|
domain = "app.test"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "./site"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "./out"
|
||||||
|
plan_root = "./plan"
|
||||||
|
|
||||||
|
[site.interactions]
|
||||||
|
route_manifest = "./routes.json"
|
||||||
|
|
||||||
|
[site.protocols.tor]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, "route manifest schema"):
|
||||||
|
load_config(config_path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user