Skip to content

Route the backend on BTP, close the app's security gaps, consume the cds-plugin - #23

Merged
oblomov-dev merged 6 commits into
mainfrom
claude/cap2ui5-analysis-roadmap-kcf9qc
Aug 21, 2026
Merged

Route the backend on BTP, close the app's security gaps, consume the cds-plugin#23
oblomov-dev merged 6 commits into
mainfrom
claude/cap2ui5-analysis-roadmap-kcf9qc

Conversation

@oblomov-dev

Copy link
Copy Markdown
Member

The deployed app could not have worked

mta.yaml has always declared an abap2UI5-srv destination pointing at the CAP module, and xs-app.json never routed anything to it. Its last route is a catch-all to the static HTML5 repository, so on a real BTP deployment every POST /rest/root/z2ui5 roundtrip went to the file store instead of the service — that is the entire application.

It was invisible because jest runs against a local cds server that never sees the approuter, and deploy-check deliberately skips mbt build. Routes added for the roundtrip, the OData service and /health; src/test/approuter-routes.test.js pins the contract, the route order (backend routes must beat the catch-all), the auth types, and that every destination a route names exists in mta.yaml.

csrfProtection: false on the roundtrip route is deliberate: the webapp posts with no CSRF token and never fetches one, so the approuter's default would 403 every roundtrip. CSRF is defended one layer down — see below.

Security

  • The User role is now required instead of bare authenticated-user. xs-security.json had declared the $XSAPPNAME.User scope and a role template since the beginning and nothing referenced them, so the authorization model existed on paper while every authenticated user in the subaccount passed, role collection assigned or not.
  • Security headers reach the data endpoints. Only the bootstrap page applied them; the roundtrip and the OData entities — the responses that actually carry application state — answered with no nosniff, no frame guard, and cacheable by any intermediary.
  • The request body is capped explicitly (Z2UI5_MAX_BODY, default 2mb). The action takes an @open object so CDS validates nothing, and the only limit was express's 100kb default applying by accident.
  • Draft retention had disagreed with the framework about its own TTL — this job deleted at 24h while the exit's draft_exp_time_in_hours said 4h, with nothing connecting them. The framework value is now the source of truth. It also ran on every instance, so N instances meant N concurrent hourly DELETEs over the same rows.
  • Indexes for the two non-primary-key access paths as HANA design-time artifacts: createdAt, because the hourly retention delete was a full scan whose cost grew with the very table it exists to contain, and owner, because the service projection filters on it. Verified to reach gen/db/src through cds build --production.
  • instances: 1 in mta.yaml: sticky app state lives in the serving process with no session affinity configured, so scaling out produces intermittent, hard-to-reproduce state loss. The comment says what to fix before raising it.

The app stops duplicating the framework's wiring

The package now ships its CAP wiring (cds-plugin.jssrv/cap/activate.js) and its CDS model, so srv/server.js drops ~120 lines, srv/draft-retention.js is deleted, and db/schema.cds / srv/z2ui5-service.cds import the packaged definitions rather than redeclaring them — the same import an external project writes, so this app's suite is what proves they work.

That last point was not cosmetic: CAP auto-loads a dependency's cds-plugin, so with the app also doing the wiring, retention started twice — two hourly DELETE loops over the same rows in one process. Now once, asserted.

A contradiction between two published documents

The README said "put your own apps into srv/app/" while AGENTS.md said srv/app/ is overwritten on every publish. Both now say the same thing, with a table answering it by what the reader is doing, and the README opens by stating that the generated repository is the demo, not the delivery mechanism.


builder 30 tests, app 44 tests (was 23), lint clean, cds build --production ok.

Generated by Claude Code


Generated by Claude Code

oblomov-dev and others added 6 commits August 21, 2026 16:31
ROUTING (P0) -- the deployed app could not have worked. mta.yaml has always
declared an abap2UI5-srv destination pointing at the CAP module, and
xs-app.json never routed anything to it: its last route is a catch-all to the
static HTML5 repository, so every POST /rest/root/z2ui5 roundtrip went to the
file store instead of the service. Nothing noticed, because jest runs against a
local cds server that never sees the approuter and deploy-check deliberately
skips mbt build. Routes added for the roundtrip, the OData service and /health;
src/test/approuter-routes.test.js pins the contract, the order (backend routes
must beat the catch-all), the auth types, and that every destination a route
names exists in mta.yaml.

csrfProtection is false on the roundtrip route deliberately: the webapp posts
with no CSRF token and never fetches one, so the approuter's default would 403
every roundtrip. CSRF is defended one layer down instead -- see below.

SECURITY (P1):

- The services now require the declared `User` role instead of merely
  `authenticated-user`. xs-security.json had declared the $XSAPPNAME.User scope
  and a role template since the beginning and nothing referenced them, so the
  authorization model existed on paper while every authenticated user in the
  subaccount passed, role collection assigned or not. The mocked development
  users carry the same role, so local and test flows are unchanged.
- Security headers now reach the DATA endpoints. Only the bootstrap page
  applied the framework's t_security_header; the roundtrip and the OData
  entities -- the responses that actually carry application state -- answered
  with no nosniff, no frame guard, and cacheable by any intermediary.
- The request body is capped explicitly (Z2UI5_MAX_BODY, default 2mb). The
  action takes an @OPEN object, so CDS validates nothing, and the only limit
  was express's 100kb default applying by accident rather than by decision.
- Draft retention had disagreed with the framework about its own TTL -- this
  job deleted at 24h while the exit's draft_exp_time_in_hours said 4h, with
  nothing connecting them. The framework value is now the source of truth and
  the env var overrides both. It also ran on every instance, so N instances
  meant N concurrent hourly DELETEs over the same rows; it now runs on one
  (Z2UI5_DRAFT_RETENTION_INSTANCE, default CF instance 0).
- Indexes for the two non-primary-key access paths, as HANA design-time
  artifacts in db/src: createdAt, because the hourly retention DELETE was a
  full scan whose cost grew with the very table it exists to contain, and
  owner, because the service projection filters on it. Verified to reach
  gen/db/src through `cds build --production`.
- mta.yaml declares instances: 1. Sticky app state lives in the serving
  process with no session affinity configured, so scaling out produces
  intermittent, hard-to-reproduce state loss. The comment says what to fix
  before raising it.
- dependabot gains the npm ecosystem in the app repo, which it never had -- no
  runtime dependency of the deployed app was ever proposed for update. CAP
  packages are grouped (they version together); openui5-dist is ignored,
  because that pin is a compatibility decision tracked upstream.
- npm audit runs on the shipped lock in the app repo's CI, advisory for now.

README's security section documented the role and CSRF gaps honestly; it now
documents what replaced them, and the sticky/multi-instance limitation that
remains.

builder 30 tests, app 44 tests (was 23), lint clean, cds build --production ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL
The package now ships its CAP wiring (cds-plugin.js -> srv/cap/activate.js)
and its CDS model, so this app stopped hand-rolling all of it:

- srv/server.js drops ~120 lines -- identity, the draft store, retention, the
  GET/HEAD bootstrap routes and the /resources mount all come from the plugin.
  What is left is genuinely app-specific: srv/app discovery, the request body
  cap, security headers on the data endpoints, and /health.
- srv/draft-retention.js is deleted; the package ships it.
- db/schema.cds and srv/z2ui5-service.cds import the packaged model and service
  rather than redeclaring them, which is the same import an external project
  writes -- so this app's test suite is what proves the packaged definitions
  work, instead of them being exercised only in theory.
- srv/z2ui5-service.js no longer registers the roundtrip handler; the plugin
  does, on whichever service declares a z2ui5 action.

That last point was not cosmetic. CAP auto-loads a dependency's cds-plugin, so
with the app ALSO doing the wiring, retention started twice -- two hourly
DELETE loops over the same rows in one process. Now once, asserted.

Also resolves a contradiction two published documents had about the one folder
users are told to write in: the README said "put your own apps into srv/app/"
while AGENTS.md said srv/app/ is overwritten on every publish. Both now say the
same thing, with a table that answers it by what the reader is doing, and the
README opens by stating that this repository is the demo rather than the
delivery mechanism.

builder 30 tests, app 44 tests, lint clean, cds build --production ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL
Picks up the CP/NP/IN comparison fixes, the escaping helpers and the CAP
entry points. builder 30 tests, app 44 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL
Picks up the removal of upstream's S-RTTI package (12 unused classes whose
factory returned null for every type kind) and the repair of
z2ui5_cl_util_json_fltr. Core drops from 318 to 306 files.

builder 30 tests, app 44 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL
Picks up the CP/NP/IN lowering finally reaching the samples tree, the
brace-balanced calling-convention parser, and the frontend patcher that fails
instead of silently shipping unpatched.

builder 30 tests, app 44 tests, lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL
Picks up the exported UNCAUGHT_EXCEPTION_PREFIX contract the smoke gate now
reads instead of a copied literal.

builder 30 tests, app 44 tests, lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL
Copilot AI lite review requested due to automatic review settings August 21, 2026 20:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@oblomov-dev
oblomov-dev merged commit 83dbd14 into main Aug 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants