From 08b51e511da0c3079957341e7228dfae88fb6be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 17 Sep 2026 23:04:13 +0200 Subject: [PATCH] One Fuseki holds every dataspace role as its own dataset, the default stack. fuseki-admin and fuseki-end-user become a single fuseki service mounting ./fuseki, whose config/fuseki/config.ttl declares an end-user and an admin dataset per dataspace over their own TDB2 folders - named after the dataspace origin with the deployment host dropped and the role appended: end-user, admin, northwind-traders.demo.end-user, northwind-traders.demo.admin. system.trig gives northwind-traders its own services in place of reusing root's, and points every service at fuseki:3030//; each dataset carries the 60 s query timeout on its query endpoint. The heap and mem_limit are the former two combined (2304m in 4608m). No entrypoint change: it already loads each app into the store URL its own service declares, and CI/dev start empty and load fixtures. varnish-admin and varnish-end-user are kept, both now fronting the single fuseki (their VCL .host default becomes fuseki): the admin/end-user split is a boundary the platform already routes on, the two role VCLs stay cohesive (admin xkey/XKEY-PURGE, end-user query-ban) and their separate storage pools keep end-user query churn from evicting the hot admin cache - merging would rebuild that isolation inside one container by parsing dataset names. egress is unchanged; both stores already route SERVICE/LOAD through it. The tests follow the single server. http-tests/config/system.trig points root and the test dataspace at fuseki:3030/end-user/ and /admin/ - test keeps sharing root's datasets, so the acl/* tests still exercise the platform's base-URI filtering of a fake admin.test.localhost authorization rather than relying on physical separation. run.sh, the http-tests and debug compose overrides, ui-tests/lib/stack.mjs and bin/sitemap/generate-sitemap.sh drop the 3031 end-user port and address datasets by path on the one 3030; the internal-SERVICE tests (GET/POST-sparql-service-internal, PATCH-service-internal, import-rdf-service-internal, GET-proxied-internal-403) target fuseki:3030/admin/ so they still prove an internal dataset is refused. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 6 +- bin/sitemap/generate-sitemap.sh | 18 +++--- config/fuseki/config.ttl | 57 ++++++++++++++++--- config/system.trig | 44 ++++++++++---- docker-compose.debug.yml | 7 +-- docker-compose.yml | 35 +++--------- http-tests/config/system.trig | 24 ++++---- http-tests/docker-compose.http-tests.yml | 9 +-- .../PATCH-service-internal.sh | 2 +- .../imports/import-rdf-service-internal.sh | 2 +- http-tests/imports/rdf-service-internal.rq | 2 +- http-tests/proxy/GET-proxied-internal-403.sh | 4 +- http-tests/run.sh | 4 +- .../query/GET-sparql-service-internal.sh | 2 +- .../query/POST-sparql-service-internal.sh | 2 +- ui-tests/lib/stack.mjs | 4 +- 16 files changed, 130 insertions(+), 92 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7fbe14501c..633a556924 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,9 +105,9 @@ Multiple dataspaces can share the same backend SPARQL service. The application runs as a multi-container setup: - **nginx**: Reverse proxy and SSL termination (wildcard subdomain routing for dataspaces) - **linkeddatahub**: Main Java application (Tomcat) -- **fuseki-admin/fuseki-end-user**: Separate SPARQL stores -- **egress**: Squid forward proxy for the stores' outbound requests (SPARQL `SERVICE`, `LOAD`): public destinations only, so a query cannot reach the other store, Varnish or the platform -- **varnish-frontend/varnish-admin/varnish-end-user**: Caching layers +- **fuseki**: One SPARQL server holding a TDB2 dataset per dataspace role (`config/fuseki/config.ttl`), named after the dataspace origin (deployment host dropped, role appended: `end-user`, `admin`, `northwind-traders.demo.end-user`, …), each under `fuseki//`; bound to apps in `config/system.trig` +- **egress**: Squid forward proxy for the store's and platform's outbound requests (SPARQL `SERVICE`, `LOAD`): public destinations only, so a query cannot reach another dataset, Varnish or the platform +- **varnish-frontend/varnish-admin/varnish-end-user**: Caching layers (admin and end-user caches both front the single `fuseki`) ### Data Flow 1. Requests come through nginx proxy diff --git a/bin/sitemap/generate-sitemap.sh b/bin/sitemap/generate-sitemap.sh index da69a25a8a..d481fa1316 100755 --- a/bin/sitemap/generate-sitemap.sh +++ b/bin/sitemap/generate-sitemap.sh @@ -1,20 +1,20 @@ -# This script directly queries the fuseki-admin and fuseki-end-user endpoints exposed on localhost, as the entrypoint does -# inside the stack. You can expose them in docker-compose.override.yml like this: -# fuseki-admin: +# This script directly queries the single fuseki endpoint exposed on localhost, as the entrypoint does inside the +# stack. You can expose it in docker-compose.override.yml like this: +# fuseki: # ports: # - 3030:3030 -# fuseki-end-user: -# ports: -# - 3031:3030 # -# One dataspace per run, because a sitemap may only list the documents of the origin serving it. Pass that origin: +# One dataspace per run, because a sitemap may only list the documents of the origin serving it. Pass that origin +# (its host, minus the deployment host `.localhost`, is the dataset name; the root origin's datasets are end-user/admin): # ./generate-sitemap.sh https://northwind-traders.demo.localhost:4443 origin="${1:-https://localhost:4443}" admin_origin=$(echo "$origin" | sed 's|://|://admin.|') -admin_endpoint="http://localhost:3030/ds/" -end_user_endpoint="http://localhost:3031/ds/" +# the dataspace's datasets in the single fuseki: ., or plain end-user/admin for root +prefix=$(echo "$origin" | sed -E 's|^https?://||; s|:[0-9]+$||; s|\.?localhost$||') +admin_endpoint="http://localhost:3030/${prefix:+$prefix.}admin/" +end_user_endpoint="http://localhost:3030/${prefix:+$prefix.}end-user/" admin_base="${admin_origin}/" envsubst '$admin_base' < ../../platform/sitemap/public-rules.rq > public-rules.rq diff --git a/config/fuseki/config.ttl b/config/fuseki/config.ttl index f4c59618a1..30e6fc3b3b 100644 --- a/config/fuseki/config.ttl +++ b/config/fuseki/config.ttl @@ -6,12 +6,51 @@ PREFIX tdb2: [] a fuseki:Server . -<#service> a fuseki:Service ; - fuseki:name "ds" ; - fuseki:endpoint [ fuseki:operation fuseki:query; ] ; - fuseki:endpoint [ fuseki:operation fuseki:update;] ; - fuseki:endpoint [ fuseki:operation fuseki:gsp-rw; ] ; - fuseki:dataset <#dataset> . - -<#dataset> a tdb2:DatasetTDB2 ; - tdb2:location "/fuseki/databases/DB2" . \ No newline at end of file +# One dataset per dataspace role, named after the dataspace's origin with the deployment host dropped and the role +# appended: https://northwind-traders.demo.localhost -> northwind-traders.demo.end-user, and its admin origin -> +# northwind-traders.demo.admin. The root dataspace's prefix is empty, hence plain end-user and admin. Each dataset +# is stored in its own folder under ./fuseki//DB2. + +# Queries are cancelled after 60 s, the Varnish backends' first_byte_timeout, past which no proxied client is still +# waiting for the answer. The timeout sits on the query endpoint: Fuseki honours ja:context there and on the +# dataset, but silently ignores it on the fuseki:Service. + +<#end-user> a fuseki:Service ; + fuseki:name "end-user" ; + fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ; + fuseki:endpoint [ fuseki:operation fuseki:update ] ; + fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ; + fuseki:dataset <#end-user-dataset> . + +<#end-user-dataset> a tdb2:DatasetTDB2 ; + tdb2:location "/fuseki/databases/end-user/DB2" . + +<#admin> a fuseki:Service ; + fuseki:name "admin" ; + fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ; + fuseki:endpoint [ fuseki:operation fuseki:update ] ; + fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ; + fuseki:dataset <#admin-dataset> . + +<#admin-dataset> a tdb2:DatasetTDB2 ; + tdb2:location "/fuseki/databases/admin/DB2" . + +<#northwind-traders.demo.end-user> a fuseki:Service ; + fuseki:name "northwind-traders.demo.end-user" ; + fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ; + fuseki:endpoint [ fuseki:operation fuseki:update ] ; + fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ; + fuseki:dataset <#northwind-traders.demo.end-user-dataset> . + +<#northwind-traders.demo.end-user-dataset> a tdb2:DatasetTDB2 ; + tdb2:location "/fuseki/databases/northwind-traders.demo.end-user/DB2" . + +<#northwind-traders.demo.admin> a fuseki:Service ; + fuseki:name "northwind-traders.demo.admin" ; + fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ; + fuseki:endpoint [ fuseki:operation fuseki:update ] ; + fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ; + fuseki:dataset <#northwind-traders.demo.admin-dataset> . + +<#northwind-traders.demo.admin-dataset> a tdb2:DatasetTDB2 ; + tdb2:location "/fuseki/databases/northwind-traders.demo.admin/DB2" . diff --git a/config/system.trig b/config/system.trig index 7a99bd3907..9ad9173247 100644 --- a/config/system.trig +++ b/config/system.trig @@ -24,9 +24,9 @@ a sd:Service ; dct:title "LinkedDataHub admin service" ; sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore . + sd:endpoint ; + a:graphStore ; + a:quadStore . } # root end-user - type + service binding @@ -59,23 +59,47 @@ a sd:Service ; dct:title "LinkedDataHub service" ; sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore . + sd:endpoint ; + a:graphStore ; + a:quadStore . } -# northwind-traders admin - type + service binding (reuses root admin service) +# northwind-traders admin - type + service binding { a lapp:AdminApplication ; - ldt:service . + ldt:service . +} + +# northwind-traders admin - service description + + +{ + a sd:Service ; + dct:title "Northwind Traders admin service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore . } -# northwind-traders end-user - type + service binding (reuses root end-user service) +# northwind-traders end-user - type + service binding { a lapp:EndUserApplication ; - ldt:service . + ldt:service . +} + +# northwind-traders end-user - service description + + +{ + a sd:Service ; + dct:title "Northwind Traders service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore . } diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index 98488f4646..f1efd718b1 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -8,9 +8,6 @@ services: - CATALINA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75 --add-exports java.base/sun.security.tools.keytool=ALL-UNNAMED -XX:+UseLinuxPosixThreadCPUClocks -agentpath:/var/linkeddatahub/profiler/lib/deployed/jdk16/linux-amd64/libprofilerinterface.so=/var/linkeddatahub/profiler/lib,5140 # heap will use up to 75% of container's RAM volumes: - ./platform/profiler:/var/linkeddatahub/profiler - fuseki-admin: + fuseki: ports: - - 3030:3030 - fuseki-end-user: - ports: - - 3031:3030 \ No newline at end of file + - 3030:3030 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6552105a58..ed7a1e8e1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,8 +39,7 @@ services: build: . mem_limit: 2048m depends_on: - - fuseki-admin - - fuseki-end-user + - fuseki - sef-compiler - egress environment: @@ -120,41 +119,25 @@ services: restart: on-failure expose: - 8080 # internal only - the platform is the sole client - fuseki-admin: + fuseki: # one server; every dataspace role is its own dataset in config/fuseki/config.ttl, stored under ./fuseki/ image: atomgraph/fuseki:6.1.0 user: root # otherwise fuseki user does not have permissions to the mounted folder which is owner by root - mem_limit: 1536m # leave headroom above heap for TDB mmap/native memory + mem_limit: 4608m # the former fuseki-admin (1536m) and fuseki-end-user (3072m) combined restart: on-failure depends_on: - egress environment: - - JAVA_OPTIONS=-Xmx768m -Xms768m + - JAVA_OPTIONS=-Xmx2304m -Xms2304m # the former heaps combined: 768m admin + 1536m end-user # every outbound request (SPARQL SERVICE, LOAD) goes through egress, which only lets it reach public addresses. # JAVA_TOOL_OPTIONS rather than JAVA_OPTIONS, which an override setting the heap would replace. The empty - # nonProxyHosts is load-bearing: by default the JVM bypasses the proxy for localhost + # nonProxyHosts is load-bearing: by default the JVM bypasses the proxy for localhost, and + # SERVICE would read a sibling dataset in this same server - JAVA_TOOL_OPTIONS=-Dhttp.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttp.proxyPort=3128 -Dhttps.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts= expose: - 3030 volumes: - ./config/fuseki/config.ttl:/fuseki/config.ttl:ro - - ./fuseki/admin:/fuseki/databases - command: [ "--config", "/fuseki/config.ttl" ] - fuseki-end-user: - image: atomgraph/fuseki:6.1.0 - user: root # otherwise the fuseki user does not have permissions to the mounted folder which is owner by root - mem_limit: 3072m # leave headroom above heap for TDB mmap/native memory - restart: on-failure - depends_on: - - egress - environment: - - JAVA_OPTIONS=-Xmx1536m -Xms1536m - # see fuseki-admin: outbound requests only reach public addresses, and not localhost either - - JAVA_TOOL_OPTIONS=-Dhttp.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttp.proxyPort=3128 -Dhttps.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts= - expose: - - 3030 - volumes: - - ./config/fuseki/config.ttl:/fuseki/config.ttl:ro - - ./fuseki/end-user:/fuseki/databases + - ./fuseki:/fuseki/databases command: [ "--config", "/fuseki/config.ttl" ] egress: # forward proxy for the triplestores' outbound requests: refuses loopback, private and link-local destinations image: ubuntu/squid:6.6-24.04_beta @@ -497,7 +480,7 @@ configs: import xkey; backend default { - .host = "${VARNISH_ADMIN_BACKEND_HOST:-fuseki-admin}"; + .host = "${VARNISH_ADMIN_BACKEND_HOST:-fuseki}"; .port = "${VARNISH_ADMIN_BACKEND_PORT:-3030}"; .first_byte_timeout = 60s; } @@ -580,7 +563,7 @@ configs: import std; backend default { - .host = "${VARNISH_END_USER_BACKEND_HOST:-fuseki-end-user}"; + .host = "${VARNISH_END_USER_BACKEND_HOST:-fuseki}"; .port = "${VARNISH_END_USER_BACKEND_PORT:-3030}"; .first_byte_timeout = 60s; } diff --git a/http-tests/config/system.trig b/http-tests/config/system.trig index 7294196b21..4a08ed2c0a 100644 --- a/http-tests/config/system.trig +++ b/http-tests/config/system.trig @@ -23,9 +23,9 @@ a sd:Service ; dct:title "LinkedDataHub admin service" ; sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore . + sd:endpoint ; + a:graphStore ; + a:quadStore . } # root end-user - type + service binding @@ -43,9 +43,9 @@ a sd:Service ; dct:title "LinkedDataHub service" ; sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore . + sd:endpoint ; + a:graphStore ; + a:quadStore . } # test admin - type + service binding @@ -63,9 +63,9 @@ a sd:Service ; dct:title "Test admin service" ; sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore . + sd:endpoint ; + a:graphStore ; + a:quadStore . } # test end-user - type + service binding @@ -83,7 +83,7 @@ a sd:Service ; dct:title "Test service" ; sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore . + sd:endpoint ; + a:graphStore ; + a:quadStore . } diff --git a/http-tests/docker-compose.http-tests.yml b/http-tests/docker-compose.http-tests.yml index 5bd7412ab1..4fcbc0e870 100644 --- a/http-tests/docker-compose.http-tests.yml +++ b/http-tests/docker-compose.http-tests.yml @@ -20,13 +20,8 @@ services: - ./http-tests/ssl/server:/var/linkeddatahub/ssl/server - ./http-tests/ssl/owner:/var/linkeddatahub/ssl/owner - ./http-tests/ssl/secretary:/var/linkeddatahub/ssl/secretary - fuseki-admin: + fuseki: ports: - 3030:3030 #volumes: - # -./http-tests/data/admin:/fuseki/databases - fuseki-end-user: - ports: - - 3031:3030 - #volumes: - # - ./http-tests/data/end-user:/fuseki/databases + # - ./http-tests/data:/fuseki/databases diff --git a/http-tests/document-hierarchy/PATCH-service-internal.sh b/http-tests/document-hierarchy/PATCH-service-internal.sh index 5ecbbc188d..d7fdeff0a7 100755 --- a/http-tests/document-hierarchy/PATCH-service-internal.sh +++ b/http-tests/document-hierarchy/PATCH-service-internal.sh @@ -30,7 +30,7 @@ item=$(ldh create item \ --title "SERVICE PATCH target" \ --container "$END_USER_BASE_URL") -endpoint="http://fuseki-admin:3030/ds/" +endpoint="http://fuseki:3030/admin/" update=$(cat <' <<< "$document"; then - echo "SERVICE in an import mapping returned data" + echo "SERVICE in an import mapping returned data" exit 1 fi diff --git a/http-tests/imports/rdf-service-internal.rq b/http-tests/imports/rdf-service-internal.rq index 8db5e40e69..c3c0a49064 100644 --- a/http-tests/imports/rdf-service-internal.rq +++ b/http-tests/imports/rdf-service-internal.rq @@ -22,7 +22,7 @@ WHERE OPTIONAL { - SERVICE SILENT + SERVICE SILENT { SELECT ?g { diff --git a/http-tests/proxy/GET-proxied-internal-403.sh b/http-tests/proxy/GET-proxied-internal-403.sh index 79ab8deac5..ca0235bf6a 100755 --- a/http-tests/proxy/GET-proxied-internal-403.sh +++ b/http-tests/proxy/GET-proxied-internal-403.sh @@ -16,14 +16,14 @@ ldh admin add agent \ "${ADMIN_BASE_URL}acl/groups/readers/" # LNK-009: Test that internal Docker services are blocked via SSRF protection -# Attempt to access the internal fuseki-admin SPARQL endpoint via the proxy +# Attempt to access the internal fuseki admin dataset endpoint via the proxy # This should be blocked and return 400 Bad Request http_status=$(curl -k -s -o /dev/null -w "%{http_code}" \ -G \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -H 'Accept: application/n-triples' \ - --data-urlencode "uri=http://fuseki-admin:3030/ds" \ + --data-urlencode "uri=http://fuseki:3030/admin" \ "$END_USER_BASE_URL" || true) # Verify that access was rejected (400) diff --git a/http-tests/run.sh b/http-tests/run.sh index 3f84e4113e..4b91d1b88f 100755 --- a/http-tests/run.sh +++ b/http-tests/run.sh @@ -230,8 +230,8 @@ export -f purge_cache export HTTP_TEST_ROOT="$PWD" export TEST_RESULTS_DIR="${TEST_RESULTS_DIR:-$HTTP_TEST_ROOT/out}" mkdir -p "$TEST_RESULTS_DIR" -export END_USER_ENDPOINT_URL="http://localhost:3031/ds/" -export ADMIN_ENDPOINT_URL="http://localhost:3030/ds/" +export END_USER_ENDPOINT_URL="http://localhost:3030/end-user/" +export ADMIN_ENDPOINT_URL="http://localhost:3030/admin/" export END_USER_BASE_URL="https://localhost:4443/" export ADMIN_BASE_URL="https://admin.localhost:4443/" export END_USER_VARNISH_SERVICE="varnish-end-user" diff --git a/http-tests/sparql-protocol/query/GET-sparql-service-internal.sh b/http-tests/sparql-protocol/query/GET-sparql-service-internal.sh index 85d8228dd7..e9fb1797d7 100755 --- a/http-tests/sparql-protocol/query/GET-sparql-service-internal.sh +++ b/http-tests/sparql-protocol/query/GET-sparql-service-internal.sh @@ -14,7 +14,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # SERVICE SILENT turns a refused call into a single empty solution, so the results are checked for data rather than # the response for a status code: a target that answered binds ?g -for endpoint in "http://fuseki-admin:3030/ds/" "http://varnish-admin/ds/" "http://localhost:3030/ds/" +for endpoint in "http://fuseki:3030/admin/" "http://varnish-admin/admin/" "http://localhost:3030/admin/" do results=$(curl -k -f -s -G \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ diff --git a/http-tests/sparql-protocol/query/POST-sparql-service-internal.sh b/http-tests/sparql-protocol/query/POST-sparql-service-internal.sh index 17cb41637e..2d50f6535e 100755 --- a/http-tests/sparql-protocol/query/POST-sparql-service-internal.sh +++ b/http-tests/sparql-protocol/query/POST-sparql-service-internal.sh @@ -11,7 +11,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # through SPARQL SERVICE either. SERVICE SILENT turns a refused call into a single empty solution, so a bound ?g means # the admin store answered -endpoint="http://fuseki-admin:3030/ds/" +endpoint="http://fuseki:3030/admin/" results=$(curl -k -f -s \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ diff --git a/ui-tests/lib/stack.mjs b/ui-tests/lib/stack.mjs index d5a4c736a2..ed3ba06403 100644 --- a/ui-tests/lib/stack.mjs +++ b/ui-tests/lib/stack.mjs @@ -10,8 +10,8 @@ export const repoRoot = fileURLToPath(new URL('../..', import.meta.url)); export const endUserBase = process.env.END_USER_BASE_URL ?? 'https://localhost:4443/'; export const adminBase = process.env.ADMIN_BASE_URL ?? 'https://admin.localhost:4443/'; -export const endUserEndpoint = process.env.END_USER_ENDPOINT_URL ?? 'http://localhost:3031/ds'; -export const adminEndpoint = process.env.ADMIN_ENDPOINT_URL ?? 'http://localhost:3030/ds'; +export const endUserEndpoint = process.env.END_USER_ENDPOINT_URL ?? 'http://localhost:3030/end-user'; +export const adminEndpoint = process.env.ADMIN_ENDPOINT_URL ?? 'http://localhost:3030/admin'; // ldh reads the PKCS12 keystore; the PEM beside it is what curl -E takes. Playwright // wants the keystore too, so the suite never touches the PEM.