This page lists all major endpoints exposed by the application for manual testing and smoke verification.
| URL | Description | Auth |
|---|---|---|
/ |
Homepage — tutorial navigator (Vue 3 component) | None |
/tutorials/{slug} |
Individual tutorial (HTML served from HANA BLOB via CAP) | None |
/concepts/{slug} |
Knowledge-graph concept landing page (HTML served from HANA BLOB via CAP; 404 if slug not in PublishedConcepts) | None |
/explore/ |
Interactive knowledge-graph visualization (Vue+Sigma.js, CAP-rendered shell with inline graph JSON). Mobile (<768px) falls back to typed-list view. | None |
/app-space |
Event-themed tutorial space (Vue 3 AppSpace component) | None |
/event-display |
Real-time event display dashboard launcher | None |
/missions/{slug} |
Mission detail page (generated by Hugo at build time) | None |
/groups/{slug} |
Group/completion-path detail page | None |
/me/ |
Profile + Recent Activity timeline (public Hugo page; calls authenticated APIs from JS) | None (page) / XSUAA (API calls) |
The QA channel is a separate author-preview surface backed by a dedicated CAP app (tutorials-srv-qa) and HDI container (tutorials-hana-qa). All routes require the Tutorial.Author XSUAA scope.
| URL | Description | Auth |
|---|---|---|
/tutorials-qa/{slug} |
QA-channel tutorial preview (yellow "QA preview" banner injected) | XSUAA + Tutorial.Author |
/tutorials-qa/_nav.json |
Navigation metadata for QA-channel tutorials | XSUAA + Tutorial.Author |
/tutorials-qa/search/... |
QA-channel search (rewrites to /qa/search/...) |
XSUAA + Tutorial.Author |
/qa-search/... |
Direct QA-search alias (rewrites to /search/...) |
XSUAA + Tutorial.Author |
Bootstrap: the role collection "Tutorials Author" must be assigned via BTP cockpit; users must log out / back in for the new scope to appear in their JWT. Until then,
/tutorials-qa/*returns 403.
| URL | Description | Auth |
|---|---|---|
/display-app/ |
Standalone event dashboard (rotating views: Board, Statistics, Leaderboard) | XSUAA |
/display-app/index.html |
Direct entry point | XSUAA |
The display-app is a standalone Vue 3 + Vite application deployed as static files at
approuter/static/display-app/. It connects via Socket.IO on the/ws/displaynamespace for real-time updates. Used on big monitors at SAP events.
All admin routes require the Admin XSUAA scope.
| URL | Description | Auth |
|---|---|---|
/admin-ui/ |
Admin shell — sap.tnt.ToolPage with side navigation |
XSUAA + Admin |
/admin-ui/components/events/ |
Events management (Fiori Elements) | XSUAA + Admin |
/admin-ui/components/missions/ |
Missions management | XSUAA + Admin |
/admin-ui/components/groups/ |
Groups / completion paths management | XSUAA + Admin |
/admin-ui/components/tutorials/ |
Tutorials management | XSUAA + Admin |
/admin-ui/components/tags/ |
Tags management | XSUAA + Admin |
/admin-ui/components/accomplishments/ |
Accomplishments management | XSUAA + Admin |
/admin-ui/components/prizes/ |
Prizes management | XSUAA + Admin |
/admin-ui/components/operations/ |
Operations dashboard | XSUAA + Admin |
/admin-ui/components/accounts/ |
User accounts management | XSUAA + Admin |
/admin-ui/components/changelog/ |
Change tracking log | XSUAA + Admin |
All drawings routes require the Admin XSUAA scope.
| URL | Description | Auth |
|---|---|---|
/drawings-ui/ |
Standalone event drawing app — operator selects event, draws random winner from eligible prize claimants, reveals winner name and email | XSUAA + Admin |
The drawings-ui is a Vue 3 application deployed as static files at
approuter/static/drawings-ui/. It calls theDrawingServiceat/drawingsto list events with claimed prizes, query the eligible winner pool, and perform the random winner selection. Winner contact information (displayName, email) is returned directly and gated by the Admin scope due to PII exposure.
| URL | Description | Auth |
|---|---|---|
/analytics-ui/ |
Vue 3 SPA with entity browser + SQL editor over AnalyticsService |
XSUAA + Admin |
Backed by
AnalyticsServiceat/admin/analytics. SQL tab uses Monaco (lazy-loaded); queries pass throughsrv/lib/analytics-sql-validator.cjs(SELECT-only, allowlisted tables, wrapped withLIMIT 5001). Entity browser is gated by@analytics.exposedannotations on CDS views/entities.
All scanner routes require the MobileApp XSUAA scope.
| URL | Description | Auth |
|---|---|---|
/scanner-ui/ |
Badge Scanner — UI5 implementation (sap.ndc.BarcodeScanner) |
XSUAA + MobileApp |
/scanner-vue/ |
Badge Scanner — Vue 3 implementation (native BarcodeDetector API) |
XSUAA + MobileApp |
Both scanner apps provide the same functionality: scan a contestant badge QR code, display progress stats, and claim prizes. The Vue version is mobile-optimized with camera-first UX and SAP Fundamental Styles. Falls back to manual JSON input on browsers without
BarcodeDetectorsupport.
| URL | Method | Description | Auth |
|---|---|---|---|
/scanner/getContestant(accountNumber='...') |
GET | OData function — contestant stats + prize info | XSUAA |
/scanner/claimPrize(recordId='...') |
GET | OData function — mark prize as CLAIMED | XSUAA |
These are only accessible when hitting the CAP backend directly (http://localhost:4004), not through the AppRouter.
| URL | Description | Auth |
|---|---|---|
http://localhost:4004/ |
CAP index page — lists all services, entities, and links | None |
/api/$api-docs/ |
Swagger UI for DeveloperService | None |
/admin/$api-docs/ |
Swagger UI for AdminService | None |
/admin/analytics/$api-docs/ |
Swagger UI for AnalyticsService | None |
/admin/exports/$api-docs/ |
Swagger UI for ExportsService | None |
/display/$api-docs/ |
Swagger UI for DisplayService | None |
/api/v1/$api-docs/ |
Swagger UI for ConsolidationService | None |
/search/$api-docs/ |
Swagger UI for SearchService | None |
/chat/$api-docs/ |
Swagger UI for ChatService (no entities; ORD-symmetric) | None |
Note: The CAP index page is not visible through the AppRouter because the catch-all route serves Hugo static content at
/. Access it directly athttp://localhost:4004/during development.
When EXPOSE_CAP_UI=true is set on the CAP srv app, these are accessible through the AppRouter with XSUAA + Admin scope:
| URL | Description | Auth |
|---|---|---|
/_dev |
CAP index page (lists all services) | XSUAA + Admin |
/_dev/api/$api-docs/ |
Swagger UI for DeveloperService | XSUAA + Admin |
/_dev/admin/$api-docs/ |
Swagger UI for AdminService | XSUAA + Admin |
/_dev/admin/analytics/$api-docs/ |
Swagger UI for AnalyticsService | XSUAA + Admin |
/_dev/admin/exports/$api-docs/ |
Swagger UI for ExportsService | XSUAA + Admin |
/_dev/display/$api-docs/ |
Swagger UI for DisplayService | XSUAA + Admin |
/_dev/api/v1/$api-docs/ |
Swagger UI for ConsolidationService | XSUAA + Admin |
/_dev/search/$api-docs/ |
Swagger UI for SearchService | XSUAA + Admin |
/_dev/chat/$api-docs/ |
Swagger UI for ChatService | XSUAA + Admin |
Enable:
cf set-env tutorials-srv EXPOSE_CAP_UI true && cf restart tutorials-srvDisable:cf unset-env tutorials-srv EXPOSE_CAP_UI && cf restart tutorials-srvDo NOT set this on production.
| URL | Description | Auth |
|---|---|---|
/api/$metadata |
DeveloperService OData metadata (EDMX) | XSUAA |
/api/ |
DeveloperService — progress tracking, user-facing API | XSUAA |
/admin/$metadata |
AdminService OData metadata | XSUAA + Admin |
/admin/ |
AdminService — full CRUD for events, missions, etc. | XSUAA + Admin |
/admin/analytics/$metadata |
AnalyticsService OData metadata | XSUAA + Admin |
/admin/analytics/ |
AnalyticsService — @analytics.exposed entity surface + runSelectQuery action |
XSUAA + Admin |
/admin/exports/$metadata |
ExportsService OData metadata | XSUAA + Admin |
/admin/exports/ |
ExportsService — exportLegacyData(format) action |
XSUAA + Admin |
/display/$metadata |
DisplayService OData metadata | XSUAA |
/display/ |
DisplayService — read-only data for event dashboards | XSUAA |
/drawings/$metadata |
DrawingService OData metadata | XSUAA + Admin |
/drawings/ |
DrawingService — prize drawing functions (events, winner pool, random selection) | XSUAA + Admin |
/api/v1/$metadata |
ConsolidationService (v1 compat) OData metadata | XSUAA |
/api/v1/ |
ConsolidationService — legacy API compatibility | XSUAA |
/search/$metadata |
SearchService OData metadata | None |
/search/ |
SearchService — tutorial/mission search (word-boundary matching) | None |
/chat/$metadata |
ChatService OData metadata (no entities; ORD-symmetric only) | XSUAA |
| URL | Method | Description | Auth |
|---|---|---|---|
/health |
GET | Liveness check — returns { status: "ok" } |
None |
/health/db |
GET | DB connectivity check (runs SELECT 1 FROM DUMMY) |
None |
/auth/user |
GET | Current user profile (id, email, name) | XSUAA |
/api/qrcode?url=... |
GET | QR code PNG generation | XSUAA |
/api/recommendations |
GET | Personalized "what's next" recommendations (embedding centroid + co-completion blend) | XSUAA |
/build/catalog |
GET | Mission/group/tutorial catalog (JSON, for build pipeline) | None |
/build/co-completions |
GET | Co-completion graph data (used by recommendations) | None |
/build/navigator |
GET | Missions, groups (incl. standalone), tutorial→mission/group mappings, and checkpoint milestones | None |
/build/slug-mapping |
GET | Slug→ID mapping for all missions/groups | None |
/build/repo-catalog |
GET | Slug-keyed DiscoveredTutorial map (third-tier discovery fallback) |
None |
/build/repo-catalog |
POST | Write the discovered-tutorial baseline (CI-as-canonical-writer) | Bearer (CONTENT_API_KEY) |
/build/concepts |
GET | Published Knowledge-Graph concepts (slug, name, description, teaches/requires/requiredBy/relatedTo). Shares buildConceptsPayload with the CAP concept-render pipeline (GET /content/concepts-index list page + the POST /content/publish/render-concepts detail phase, #1327). |
None |
/graph/publishConcept |
POST | Admin action — sets publishedAt + publishedBy on a Concept |
XSUAA + KnowledgeGraph.Admin |
/graph/unpublishConcept |
POST | Admin action — clears publishedAt + publishedBy on a Concept |
XSUAA + KnowledgeGraph.Admin |
/graph/explore-data |
GET | Bulk graph JSON for the /explore/ page (nodes + edges + generatedAt; 5-min LRU cache) |
None |
/graph/path |
GET | Shortest path between two tutorial/concept slugs (?from=<slug>&to=<slug>). 400 on same-slug. Extracted from Phase 2 Joule tool. |
None |
/api/advocates |
GET | Public list of active developer advocates (sorted by lastName, topics + links denormalized, ETag + max-age=60, stale-while-revalidate=600) |
None |
/api/advocates/:slug/photo[?size=thumb] |
GET | Photo bytes (256-WebP default, 64-WebP with ?size=thumb) served from HANA AdvocatePhotos. ETag is the sha256, max-age=86400. 404 when the advocate has no photo or slug is unknown. |
None |
/feedback/submit |
POST | Tutorial feedback form (rate-limited; submitter IP hashed via SUBMISSION_SALT_SECRET) |
None |
/chat/stream |
POST | Joule chat streaming endpoint (Server-Sent Events) | XSUAA |
/a2a |
POST | A2A JSON-RPC 2.0 endpoint (message/send, message/stream, tasks/get, tasks/cancel) for central Joule consumption (#1220); machine-to-machine. Skill via metadata.skillId, defaults to conversational tutorial-chat. Enable/config via /admin-ui/#joule (ChatSettings a2aEnabled). Agent card at /.well-known/agent-card.json. |
IAS (client-credentials) |
/.well-known/agent-card.json |
GET | A2A Agent Card — public discovery document (5 skills, streaming, IAS security scheme). Base URL + token URL from /admin-ui/#joule (ChatSettings). |
None |
/.well-known/a2a-instructions.md |
GET | A2A consumption guide (how to authenticate + call) | None |
/.well-known/tutorial-provenance/jwks.json |
GET | Ed25519 JWKS for verifying tutorial provenance JWS tokens ({ keys: [...] }). Returns 404 when PROVENANCE_ENVELOPE_ENABLED flag is OFF. Anonymous Express route — @requires not applicable. Pair: /content/tutorials/:slug/provenance. |
None |
/api/codecheck |
POST | AI code-check spike (issue #171, gated on ChatSettings.codeCheckEnabled). Body: { tutorialSlug, stepNumber, submittedCode, language? }. Returns { verdict: 'pass'|'partial'|'fail', summary, suggestions[], correctAspects[] }. 503 when flag off; 429 with Retry-After on per-user 30/hr or per-(user,slug,step) 5/5min cap. |
XSUAA |
/author/generateOsVariants |
POST | AI-assisted OS variant generation for the VS Code authoring plugin (issue #173). Body: { sourceMarkdown, sourceOS, targetOSes[], context? }. Returns { variants[], model, tokensUsed, requestId }. 60/hr per author. See spec #173 §5. |
XSUAA + Tutorial.Author |
/admin/embeddings/stats |
GET | Tutorial embedding coverage / drift statistics | XSUAA + Admin |
/drawings/listEvents() |
GET | OData function — events with ≥1 claimed prize (newest-first). Returns { legacyId, name, startDate }. |
XSUAA + Admin |
/drawings/pool(eventId=<legacyId>) |
GET | OData function — eligible winner pool statistics for an event. Returns { eventId, eligibleCount } (count of distinct non-anonymized users with CLAIMED prize claims, excludes PII). |
XSUAA + Admin |
/drawings/draw(eventId=<legacyId>) |
GET | OData function — select one random winner and return their contact info. Returns { winner: { displayName, email } | null, eligibleCount }. Server-side winner selection; returns PII. |
XSUAA + Admin |
/api/alerts |
GET | Active alerts with audience=ALL. 60 s cache. | None |
/api/alerts/me |
GET | ALL + AUTHENTICATED + ADMIN (if admin). 30 s private cache. | XSUAA |
/api/homepage/events |
GET | 3-4 upcoming events (merged from DB + events calendar). 60 s cache. | None |
/api/homepage/videos |
GET | @sapdevs video feed (Developer News + recent uploads). 15-min cache; requires YOUTUBE_API_KEY. |
None |
/api/homepage/communityBlogs |
GET | SAP Community blog RSS feed (latest posts). 30-min cache. | None |
/api/homepage/news |
GET | SAP News headlines RSS feed. 30-min cache. | None |
/api/homepage/shelves?verb=<v> |
GET | All active HomepageShelves entries for one verb (LEARN|BUILD|INTEGRATE|OPERATE|AI|CONNECT). 5-min cache. |
None |
/api/homepage/redirectsActive |
GET | Active LegacyRedirects rows (approuter-only consumer; refreshes hourly). |
None |
/api/homepage/recordRedirectHits |
POST | Idempotent batch hit counter for legacy redirects (approuter-only writer). Body: { hits: [{ id, count }] }. |
None |
/build/homepage-shelves |
GET | All HomepageShelves entries keyed by verb (Hugo build-time only; bakes hugo/data/homepage_shelves.json). Payload includes tagline / whyItMatters / authoringStatus (#759). |
None |
/build/verb-definitions |
GET | All VerbDefinitions rows (6 verbs) — labels, icons, taglines, "why it matters" copy for the homepage verb-spine flip tiles (#759). Bakes hugo/data/verb_definitions.json. |
None |
/build/shelf-definitions |
GET | All ShelfDefinitions rows (4 shelf categories: LEARN / START / REFERENCE / COMMUNITY) — taglines + "why it matters" copy for verb sub-page shelf headers (#759). Bakes hugo/data/shelf_definitions.json. |
None |
/build/navigatorreturns a shape withmissions[],groups[](incl. standalone published Groups),tutorialMappings[], andcheckpointMappings[](milestone markers). The 5-minute in-memory cache is automatically invalidated when the Admin UI saves changes to Missions, Groups, or CompletionPath entities — no?nocache=1needed. Implementation: srv/lib/navigator-catalog.js.
/feedback/submitreturns 503 whenSUBMISSION_SALT_SECRETis missing — set it in CI secrets and locally before testing the form. The request body is capped at 8 KB.
| URL | Method | Description | Auth |
|---|---|---|---|
/content/tutorials/{slug} |
GET | Serve tutorial HTML from HANA (ETag, Cache-Control) | None |
/content/tutorials/{slug}/provenance |
GET | Signed provenance JWS for a tutorial ({ jws, jwks_url }). jws is a compact Ed25519-signed JWS attesting contentHash, sourceCommit, builtAt, and freshness (confidence: high/medium/low/unknown). Returns 404 when PROVENANCE_ENVELOPE_ENABLED flag is OFF or slug unknown. Anonymous Express route — @requires not applicable. Pair: /.well-known/tutorial-provenance/jwks.json. Requires PROVENANCE_SIGNING_KEY credstore secret (Ed25519 PKCS8 PEM). |
None |
/content/hashes |
GET | SHA-256 map of active content ({ slug: hash }) |
None |
/content/nav |
GET | Navigation metadata for published tutorials | None |
/content/publish |
POST | Deprecated — single-shot publish (base64-gzipped files). Kept for one release cycle; new clients use the chunked protocol below. | Bearer (CONTENT_API_KEY) |
/content/publish/begin |
POST | Open a chunked publish session. Returns { sessionId, version, expiresAt } (201) or 409 if a publish is already in progress. |
Bearer (CONTENT_API_KEY) |
/content/publish/append |
POST | Append a batch of files to an open session. Files shape: { slug: base64gzip }. Idempotent for (sessionId, slug). |
Bearer (CONTENT_API_KEY) |
/content/publish/commit |
POST | Activate the session's manifest. Idempotent (returns alreadyActive: true on repeat). |
Bearer (CONTENT_API_KEY) |
/content/publish/abort |
POST | Discard an open session. Idempotent. | Bearer (CONTENT_API_KEY) |
/content/code-check-specs |
POST | AI code-check spike (issue #171). Upserts CodeCheckSpecs rows from *.codecheck.json sidecars produced by fetch-tutorials. Body: { specs: [{ slug, stepNumber, goal, language?, hints?, referenceSolution? }] }. Carry-forward semantics (no DELETE on absent specs). Server-only — referenceSolution never reaches the client. |
Bearer (CONTENT_API_KEY) |
/content/rollback |
POST | Revert to previous manifest version | Bearer (CONTENT_API_KEY) |
The chunked endpoints replace single-shot POST /content/publish. They split a publish across many small batches so a flaky TCP connection or a 53 MB JSON body doesn't kill the whole run, and the server's commit step does carry-forward of unchanged slugs (so a partial payload no longer drops the rest of the catalog).
Typical client flow:
# 1. Begin a session — server returns sessionId + version
curl -sX POST http://localhost:4004/content/publish/begin \
-H "Authorization: Bearer $CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "trigger": "manual", "hugoVersion": "0.x" }'
# → 201 { "sessionId": "...", "version": 42, "expiresAt": "..." }
# 2. Append batches (50 slugs at a time by default; idempotent per (sessionId, slug))
curl -sX POST http://localhost:4004/content/publish/append \
-H "Authorization: Bearer $CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sessionId": "...", "files": { "slug-a": "<base64gzip>", "slug-b": "<base64gzip>" } }'
# 3. Commit — activates the manifest; idempotent (returns alreadyActive: true on repeat)
curl -sX POST http://localhost:4004/content/publish/commit \
-H "Authorization: Bearer $CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sessionId": "..." }'
# Or, abandon the session:
curl -sX POST http://localhost:4004/content/publish/abort \
-H "Authorization: Bearer $CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sessionId": "..." }'begin returns 409 if another publish session is already in progress; abort it (or wait for the reaper, which sweeps stale sessions every 5 minutes after a 30-minute idle threshold) before retrying.
The CAP backend serves a hosted Model Context Protocol surface via the @cap-js/mcp adapter (tools-only) plus a custom compose router (srv/lib/mcp-compose-router.js) that adds resources + prompts on the RP-bearing mounts. Each CDS service is mounted separately at /mcp/<svc> — there is no aggregate /mcp root. End-user connection guide: docs/end-users/mcp-quickstart.md; parameter reference: docs/developers/reference/mcp-server.md.
| Mount | Service | Curated tools | Auth (at mount) |
|---|---|---|---|
/mcp/search |
SearchService | search_tutorials, list_missions, get_mission, get_tutorial |
None (anonymous) |
/mcp/graph |
KnowledgeGraphService | kg_shared_concepts, kg_neighborhood, kg_search_concepts, kg_community (+ resources + prompts) |
None (anonymous) |
/mcp/homepage |
HomepageService | get_my_recommended_tutorials, get_my_recommended_missions |
authenticated-user |
/mcp/api |
DeveloperService | get_my_tutorials, get_my_missions, get_my_events, get_my_completed_steps, get_tutorial_step, complete_step, reset_tutorial_progress |
authenticated-user |
/mcp/admin |
AdminService | merge_concepts, promote_community_to_mission, trigger_rebuild, publish_content (+ resources + prompts) |
Admin + per-action scope |
describe and query are auto-generated on every mount by @cap-js/mcp. Each service also serves resources (tutorial://<slug>, mission://<slug>, concept://<id>) and prompts (prompts/list) where the compose router is mounted (/mcp/graph, /mcp/admin).
The approuter (approuter/xs-app.json) fronts three additional prefixes that srv/server.js rewrites onto the real /mcp/* mounts:
| Public prefix | Approuter auth | Rewrites to | Purpose |
|---|---|---|---|
/mcp/* |
none (csrf off) | (direct) | Anonymous read tier — public services |
/mcp-pat/* |
none (csrf off) | /mcp/* |
PAT tier — requires Bearer pat_... (else JSON-RPC 401) |
/mcp-auth/* |
xsuaa, scope Tutorial.MCP |
/mcp/* |
OAuth/JWT tier — forwards the user JWT |
/mcp-admin/* |
xsuaa, scope Tutorial.MCP |
/mcp/admin/* |
Phase 3 admin-curation tools |
Kill switches: MCP_AUTH_ENABLED=false → /mcp-auth/* + /mcp-pat/* return 503. MCP_PHASE3_ENABLED=false or MCP_ADMIN_TOOLS_ENABLED=false → /mcp-admin/* returns 503. Transport is Streamable HTTP (MCP protocol 2025-06); clients negotiate JSON vs SSE via Accept.
| URL | Method | Description | Auth |
|---|---|---|---|
/pats |
GET | PatService — list the caller's own PATs (MyPATs, row-scoped to user.email = $user.id) |
XSUAA (authenticated-user) |
/pats/mintPAT |
POST | Mint a PAT: mintPAT(name, scopes, ttlDays) → { ID, token, prefix, expiresAt }. Plaintext token returned once; server stores only a SHA-256 hash. Scopes: read (read tools) / write (allows complete_step, reset_tutorial_progress). |
XSUAA (authenticated-user) |
/pats/MyPATs(<ID>)/PatService.revokePAT |
POST | Bound action — revoke one of the caller's PATs | XSUAA (authenticated-user) |
PATs are recognized by srv/lib/mcp-pat-middleware.js (SHA-256 hash lookup, 60 s cache) on the /mcp-pat/* prefix. Minting is surfaced in the Admin UI at /admin-ui/#pats for users in the Tutorials MCP Users role collection. An expired or revoked PAT returns 401. Write tools reject PATs lacking the write scope with 403.
| URL | Protocol | Description | Auth |
|---|---|---|---|
/ws/event-stream |
Socket.IO | Anonymous live tutorial-completion stream (backed by EventStreamService) |
None |
/ws/display |
Socket.IO | Display dashboard live updates (backed by DisplayService) |
XSUAA DisplayApp (enforced at namespace join, not approuter) |
/socket.io/ |
Socket.IO | Underlying transport endpoint for both namespaces (?EIO=4&transport=websocket) |
None |
Tools registered with the Joule chat runtime (LLM tool-calling surface, not standalone HTTP endpoints). Reachable via POST /chat/stream. Each tool's registration is gated on a ChatSettings flag so the LLM only sees the tool when the feature is enabled.
Author-aware branch + skip recommender for tutorials and missions (issue #172 PR 4).
-
Registration gate — registered when
ChatSettings.enabled = true && ChatSettings.branchingEnabled = true. WhenbranchingEnabled = false, the tool is not registered and the LLM falls back to general guidance. -
Params —
missionSlug?(string),tutorialSlug?(string),branchPointId?(string). At least one ofmissionSlug/tutorialSlugis required.branchPointIdrequirestutorialSlug. -
Return shape:
-
Telemetry — writes one
BranchDecisionsrow per branch / alt-group / skip recommendation withsource: 'jouleTool'. Skip-point telemetry is only emitted whenskip === true. -
Error envelopes (validation failures, no telemetry):
error: 'requires_at_least_one_of: missionSlug, tutorialSlug, branchPointId'error: 'branchPointId requires tutorialSlug'error: 'unknown_branch_point: <id>'
-
Empty-shape envelopes (not errors, no recommendation possible):
note: 'tutorial_has_no_branches'note: 'mission_not_found'note: 'mission_has_no_alt_groups'
Implementation: srv/lib/branch/joule-tool.js. Telemetry helper: srv/lib/branch/branch-telemetry.js.
Hybrid pathBetween Joule tool — translates "I want to learn X" / "what should I learn next" / "show me a path to Y" prompts into an ordered tutorial sequence (issue #445 / Phase 2 of #381).
-
Registration gate — registered when
ChatSettings.enabled = true && ChatSettings.kgPathBetweenEnabled = true. WhenkgPathBetweenEnabled = false(default), the tool is not registered and the LLM falls back to general guidance viasearchTutorials. -
Params —
toSlug(string, required, lowercase alphanumeric + hyphens, 1-80 chars),fromSlug?(string, optional — same shape). IffromSlugis omitted, the handler infers it from the user's most-recent COMPLETED TaskRecord; if no completion history, the search is unanchored (usestoSlugas its own neighborhood center). -
Return shape — rendered markdown string that the LLM paraphrases or quotes verbatim:
Here's a path from `<fromSlug>` to `<toSlug>`: 1. **<title>** — [<slug>](https://developers.sap.com/tutorials/<slug>.html) ~<minutes> min · <reason>
Where
<reason>is one of"Prerequisite chain","Often completed together","Shares concepts". Empty result set returns a friendly "couldn't find a path" message pointing at the catalog. -
Telemetry — emits
kg.joule.path_requestedat dispatch start ({ fromSlug, toSlug, hasUserId, fromSlugInferred, unanchored }) andkg.joule.path_returnedat dispatch end ({ ..., resultCount, pathTypeBreakdown: { PREREQ, CO_COMPLETED, SHARED_CONCEPT }, latencyMs, exactTargetReached, error? }). ThepathTypeBreakdownlets ops see which arm produces results post-rollout — useful for validating the Phase 2.5 prereq-enrichment hypothesis. -
Error envelopes — handler returns friendly strings for the LLM to paraphrase (never throws into the LLM-tool-result):
- Malformed
toSlug/fromSlug—"That tutorial slug doesn't look right…" SparqlTimeoutError—"I couldn't find a learning path right now — the query timed out…"Telemetry tag:error: 'timeout'.SparqlSyntaxError/ generic —"Internal error finding a learning path…"Telemetry tag:error: 'syntax'orerror: 'unknown'.
- Malformed
-
AI-judge fixture — test/hybrid/joule-tool-pick-find-path.test.js — 12-prompt fixture asserting the LLM picks the right tool (findLearningPath vs getRelevantSteps vs checkCode vs no-tool) at ≥90% accuracy. Gated by
HYBRID_AI_TESTS=true. Regression guard against tool-descriptor changes.
Implementation: srv/lib/kg/joule-tool-find-path.js + srv/lib/kg/concepts-for-user.js (user-coverage helper). Architecture details: docs/developers/architecture/joule.md.
| URL | Method | Description | Auth |
|---|---|---|---|
/login |
GET | Triggers XSUAA login redirect | XSUAA |
/login/callback |
GET | OAuth2 callback (handled by AppRouter) | XSUAA |
/admin/rebuild |
POST | Hot-reload static content (tar.gz upload) | Bearer (REBUILD_API_KEY) |
The application defines its roles in xs-security.json. Endpoints in this document indicate which scope is required where applicable.
| Scope | Role Collection | Used by |
|---|---|---|
$XSAPPNAME.Admin |
"Tutorials Admin" | /admin-ui/, /analytics-ui/, /_dev, /admin/* OData, /admin/embeddings/stats |
$XSAPPNAME.SuperAdmin |
"Tutorials SuperAdmin" | promote_community_to_mission, publish_content (emergency), other SuperAdmin-gated admin actions |
$XSAPPNAME.MobileApp |
"Tutorials Scanner" | /scanner-ui/, /scanner-vue/ |
$XSAPPNAME.Tutorial.Author |
"Tutorials Author" | /tutorials-qa/*, /qa-search/*, /author/generateOsVariants, trigger_rebuild |
$XSAPPNAME.KnowledgeGraph.Admin |
"Tutorials Admin" | /graph/publishConcept, /graph/unpublishConcept, merge_concepts |
$XSAPPNAME.Tutorial.API |
"Tutorials API Consumer" | /graphql (authenticated GraphQL over DeveloperService) |
$XSAPPNAME.Tutorial.MCP |
"Tutorials MCP Users" | /mcp-auth/*, /mcp-admin/* (hosted MCP OAuth/JWT tiers); PAT minting at /admin-ui/#pats |
| (authenticated-user) | (any logged-in user) | /api/*, /display/*, /chat/*, /pats, /mcp-pat/* |
- AppRouter:
http://localhost:5000(runsapprouter/server.js) - CAP backend:
http://localhost:4004(runscds watch) - Hugo dev server:
http://localhost:1313(runsnpm run dev)
When running locally, the approuter proxies all API calls to CAP_BASE_URL (default http://localhost:4004) and injects mock Basic auth:
/admin/*→admin:admin/display/*→display:display- All others →
developer:developer
# 1. Health
curl http://localhost:4004/health
curl http://localhost:4004/health/db
# 2. Public data (build pipeline)
curl http://localhost:4004/build/catalog | jq '.missions | length'
curl http://localhost:4004/build/navigator | jq '.tutorials | length'
curl http://localhost:4004/build/co-completions | jq '. | length'
curl http://localhost:4004/build/repo-catalog | jq '. | length'
# 3. OData metadata
curl http://localhost:4004/api/\$metadata -H "Accept: application/xml"
curl http://localhost:4004/admin/\$metadata -H "Accept: application/xml"
curl http://localhost:4004/admin/analytics/\$metadata -H "Accept: application/xml"
curl http://localhost:4004/admin/exports/\$metadata -H "Accept: application/xml"
# 4. Content serving
curl http://localhost:4004/content/hashes | jq 'keys | length'
curl -I http://localhost:4004/content/tutorials/abap-dev-get-started
# 5. Search (unauthenticated)
curl "http://localhost:4004/search/Tutorials?\$search=abap" | jq '.value | length'
# 6. Auth (via approuter)
curl http://localhost:5000/auth/user # expect 401 or user JSON
# 7. UIs (via approuter — open in browser)
open http://localhost:5000/admin-ui/ # ToolPage shell (Admin scope)
open http://localhost:5000/analytics-ui/ # SQL + entity browser (Admin scope)
open http://localhost:5000/scanner-vue/ # Badge scanner (MobileApp scope)
open http://localhost:5000/me/ # Profile + Recent Activity timelineFocused pen-test suite added in #797. All framework defaults (CAP CSRF auto-enforcement, Fiori Elements token prefetch, sanitize-html, approuter CSP) cover the primary attack surface — these tests pin the coverage against regression.
| File | Type | Scope |
|---|---|---|
test/unit/srv/analytics-sql-validator.pen.test.js |
unit | Malicious SQL against AnalyticsService.runSelectQuery validator (DDL/DML, stacked queries, comment bypasses, disallowed tables, oversize input). Covers srv/lib/analytics-sql-validator.cjs. |
test/unit/scripts/sanitize-html.pen.test.js |
unit | OWASP XSS Filter Evasion Cheat Sheet payloads against tutorial-source sanitizer. Covers scripts/parsers/sanitize-html.ts. |
test/unit/scripts/check-hugo-safe-html.test.js |
unit | Unit coverage for the Hugo safeHTML grep guard (marker recognition, 3-line window, allow/deny cases). Covers scripts/check-hugo-safe-html.cjs. |
test/smoke/csrf-enforcement.test.js |
smoke | POST to /admin/Tags, /admin/Missions, /api/completeStep without a valid x-csrf-token → 4xx. Covers the CAP OData mutation surface. |
test/smoke/express-route-mutations.test.js |
smoke | Sweeps all 10 bearer-token-protected app.post(...) routes in srv/server.js (/content/publish, /content/rollback, /content/code-check-specs, /content/validate-answer-specs, and the 6 build endpoints), plus XSUAA-scoped and public-POST hardening. |
test/smoke/xss-reflection.test.js |
smoke | Read-only reflection check: probes /homepage/ and /tutorials/tutorial-platform-feature-cookbook for unescaped payloads, /search/?q=<payload> for reflected query strings, and /tutorials/<payload>/ for reflection on the 404 path. |
test/smoke/security-headers.test.js |
smoke | CSP, X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy on approuter HTML responses. |
scripts/check-hugo-safe-html.cjs |
build guard | Fails the build if any new safeHTML / safeHTMLAttr / printf "<%s>" lands in hugo/layouts/**/*.{html,xml,xsl} without a <!-- security-reviewed: ... --> marker within 3 lines above. Runs as npm run check:security-annotations (wired into prebuild). |
Running locally:
# All unit pen tests (fast, no external deps)
npx vitest run test/unit/srv/analytics-sql-validator.pen.test.js \
test/unit/scripts/sanitize-html.pen.test.js \
test/unit/scripts/check-hugo-safe-html.test.js \
--project unit
# All smoke pen tests (needs deployed DEV; SRV and/or approuter URLs)
SMOKE_BASE_URL="https://tutorial-system-dev-tutorials-approuter.cfapps.eu10-005.hana.ondemand.com" \
SMOKE_SRV_URL="https://tutorial-system-dev-tutorials-srv.cfapps.eu10-005.hana.ondemand.com" \
npx vitest run test/smoke/csrf-enforcement.test.js \
test/smoke/express-route-mutations.test.js \
test/smoke/xss-reflection.test.js \
test/smoke/security-headers.test.js \
--project smoke
# Hugo safeHTML guard
npm run check:security-annotationsPolicy for new findings: if a pen test uncovers a real vulnerability (not a test-expectation mismatch), file a follow-up issue referencing #797, skip that individual case with a .skip and a // TODO(#XXX) comment, and address it in a dedicated PR. This suite is a regression net, not a fix-it-all PR.
{ "branchPoints": [ { "id": "...", "picked": "...", "reason": "...", "confidence": 0.0, "allBranches": [{ "key": "...", "label": "..." }] } ], "altGroups": [ { "id": "...", "groupKey": "...", "picked": "...", "reason": "...", "confidence": 0.0, "allBranches": [{ "key": "...", "label": "..." }] } ], "skipPoints": [ { "stepNumber": 4, "skip": true, "reason": "...", "skipLabel": "...", "skipReason": "..." } ], "note": "...", // empty-shape signal (see below) "error": "..." // validation failure (see below) }