cds-caching 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,7 @@ Please also read the introduction blog post: [Boosting performance in SAP Cloud
25
25
  | [Programmatic API](docs/programmatic-api.md) | Full API reference for cache operations |
26
26
  | [Protocol Support](docs/protocols.md) | Caching across OData, REST, GraphQL, HCQL, and MCP |
27
27
  | [Key Management](docs/key-management.md) | Key templates, context awareness, custom keys |
28
+ | [Security](docs/security.md) | Authorization, cache key isolation, production checklist |
28
29
  | [Metrics Guide](docs/metrics-guide.md) | Statistics, monitoring, and performance tracking |
29
30
  | [OpenTelemetry Integration](docs/telemetry.md) | Distributed tracing and metrics export |
30
31
  | [OData API](docs/odata-api.md) | REST endpoints for management and monitoring |
@@ -260,12 +261,14 @@ See **[Feature Activation](docs/feature-activation.md)** for reuse vs own activa
260
261
  | BTP with HTML5 repo | `cds add caching-metrics` + `metrics.enabled` — no `metrics.reuse.dashboard` |
261
262
  | API only, no UI | `metrics.reuse.api` or `using … index.cds` + `metrics.enabled` |
262
263
 
263
- Restrict access with a fully-qualified annotate — do **not** repeat the `using` import:
264
+ The API requires an authenticated user by default. Restrict it to an administrative role with a fully-qualified annotate — do **not** repeat the `using` import:
264
265
 
265
266
  ```cds
266
- annotate plugin.cds_caching.CachingApiService with @requires: 'authenticated-user';
267
+ annotate plugin.cds_caching.CachingApiService with @requires: 'CacheAdmin';
267
268
  ```
268
269
 
270
+ See the [Security Guide](docs/security.md) for the production checklist, including cache key isolation for user-filtered data.
271
+
269
272
  ## Multi-Tenancy (MTX)
270
273
 
271
274
  cds-caching supports SAP BTP multi-tenant applications using `@sap/cds-mtxs`. When multitenancy is detected, the plugin automatically:
@@ -102,7 +102,7 @@ sap.ui.predefine("sap/base/util/merge", ["./_merge"],function(e){"use strict";va
102
102
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
103
103
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
104
104
  */
105
- sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
105
+ sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
106
106
  /*!
107
107
  * OpenUI5
108
108
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
@@ -76,7 +76,7 @@ sap.ui.predefine("sap/base/util/resolveReference", ["sap/base/util/ObjectPath"],
76
76
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
77
77
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
78
78
  */
79
- sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
79
+ sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
80
80
  /*!
81
81
  * OpenUI5
82
82
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
@@ -61,7 +61,7 @@ sap.ui.define([
61
61
  */
62
62
  version: "1.136.0",
63
63
  // buildinfo.lastchange is deprecated and is therefore defaulted to empty string
64
- buildinfo : { lastchange : "", buildtime : "20260716-2315" }
64
+ buildinfo : { lastchange : "", buildtime : "20260816-1454" }
65
65
  };
66
66
 
67
67
  /**
@@ -3,5 +3,5 @@
3
3
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
4
4
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
5
5
  */
6
- sap.ui.define(["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
6
+ sap.ui.define(["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
7
7
  //# sourceMappingURL=Global.js.map
@@ -351,7 +351,7 @@ sap.ui.predefine("sap/base/util/uid", [],function(){"use strict";var e=0;var n=f
351
351
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
352
352
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
353
353
  */
354
- sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
354
+ sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
355
355
  /*!
356
356
  * OpenUI5
357
357
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
@@ -344,7 +344,7 @@ sap.ui.predefine("sap/base/util/uid", [],function(){"use strict";var e=0;var n=f
344
344
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
345
345
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
346
346
  */
347
- sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
347
+ sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
348
348
  /*!
349
349
  * OpenUI5
350
350
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
@@ -344,7 +344,7 @@ sap.ui.predefine("sap/base/util/uid", [],function(){"use strict";var e=0;var n=f
344
344
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
345
345
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
346
346
  */
347
- sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
347
+ sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
348
348
  /*!
349
349
  * OpenUI5
350
350
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
@@ -1657,7 +1657,7 @@ sap.ui.predefine("sap/tnt/library", ["./IllustratedMessageType","sap/ui/base/Dat
1657
1657
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
1658
1658
  * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
1659
1659
  */
1660
- sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260716-2315"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
1660
+ sap.ui.predefine("sap/ui/Global", ["sap/ui/VersionInfo","sap/base/Log","sap/base/assert","sap/base/util/ObjectPath"],function(e,a,r,t){"use strict";if(globalThis.OpenAjax&&globalThis.OpenAjax.hub){OpenAjax.hub.registerLibrary("sap","http://www.sap.com/","0.1",{})}var n;let s={version:"1.136.0",buildinfo:{lastchange:"",buildtime:"20260816-1454"}};s=Object.assign(sap.ui,s);var o=sap.ui.loader._.getSyncCallBehavior();sap.ui.getVersionInfo=function(r){if(r&&r.async){a.info("Do not use deprecated function 'sap.ui.getVersionInfo'. Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead")}else{a.warning("Do not use deprecated function 'sap.ui.getVersionInfo' synchronously! Use"+" 'sap/ui/VersionInfo' module's asynchronous .load function instead","Deprecation",null,function(){return{type:"sap.ui.getVersionInfo",name:"Global"}})}return e._load(r)};sap.ui.namespace=function(e){r(false,"sap.ui.namespace is long time deprecated and shouldn't be used");return t.create(e)};sap.ui.lazyRequire=function(e,s,i){r(typeof e==="string"&&e,"lazyRequire: sClassName must be a non-empty string");r(!s||typeof s==="string","lazyRequire: sMethods must be empty or a string");if(o===2){a.error("[nosync] lazy stub creation ignored for '"+e+"'");return}var u=e.replace(/\//gi,"."),l=u.lastIndexOf("."),c=u.substr(0,l),p=u.substr(l+1),f=t.create(c),y=f[p],d=(s||"new").split(" "),b=d.indexOf("new");i=i||u;if(!y){if(b>=0){y=function(){if(o){if(o===1){a.error("[nosync] lazy stub for constructor '"+u+"' called")}}else{a.debug("lazy stub for constructor '"+u+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function","lazyRequire: oRealClass must be a function after loading");if(t._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"'has not been replaced by module '"+i+"'")}var s=Object.create(t.prototype);if(!(this instanceof y)){n=n||sap.ui.require("sap/ui/base/Object");if(n&&s instanceof n){a.error("Constructor "+e+' has been called without "new" operator!',null,null,function(){try{throw new Error}catch(e){return e}})}}var l=t.apply(s,arguments);if(l&&(typeof l==="function"||typeof l==="object")){s=l}return s};y._sapUiLazyLoader=true;d.splice(b,1)}else{y={}}f[p]=y}d.forEach(function(e){if(!y[e]){y[e]=function(){if(o){if(o===1){a.error("[no-sync] lazy stub for method '"+u+"."+e+"' called")}}else{a.debug("lazy stub for method '"+u+"."+e+"' called.")}sap.ui.requireSync(i.replace(/\./g,"/"));var t=f[p];r(typeof t==="function"||typeof t==="object","lazyRequire: oRealClass must be a function or object after loading");r(typeof t[e]==="function","lazyRequire: method must be a function");if(t[e]._sapUiLazyLoader){throw new Error("lazyRequire: stub '"+u+"."+e+"' has not been replaced by loaded module '"+i+"'")}return t[e].apply(t,arguments)};y[e]._sapUiLazyLoader=true}})};sap.ui.lazyRequire._isStub=function(e){r(typeof e==="string"&&e,"lazyRequire._isStub: sClassName must be a non-empty string");var a=e.lastIndexOf("."),n=e.slice(0,a),s=e.slice(a+1),o=t.get(n||"");return!!(o&&typeof o[s]==="function"&&o[s]._sapUiLazyLoader)};sap.ui.resource=function(e,a){r(typeof e==="string","sLibraryName must be a string");r(typeof a==="string","sResourcePath must be a string");return sap.ui.require.toUrl((String(e).replace(/\./g,"/")+"/"+a).replace(/^\/*/,""))};sap.ui.localResources=function(e){r(e,"sNamespace must not be empty");var a={};a[e.replace(/\./g,"/")]="./"+e.replace(/\./g,"/");sap.ui.loader.config({paths:a})};return s});
1661
1661
  /*!
1662
1662
  * OpenUI5
1663
1663
  * (c) Copyright 2025 SAP SE or an SAP affiliate company.
package/cds-plugin.js CHANGED
@@ -2,11 +2,25 @@ const cds = require('@sap/cds')
2
2
  const { fs, path } = cds.utils;
3
3
  const CachingService = require('./lib/CachingService')
4
4
  const { scanCachingAnnotations } = require('./lib/util')
5
- const { getCachingRequiresEntries } = require('./lib/config-normalizer')
5
+ const { getCachingRequiresEntries, detectMisplacedKeyManagement } = require('./lib/config-normalizer')
6
6
  const { resolvePluginRoots } = require('./lib/plugin-roots')
7
7
 
8
8
  const LOG = cds.log("cds-caching");
9
9
 
10
+ /**
11
+ * Parsed project package.json, or an empty object when unreadable.
12
+ * @returns {object}
13
+ */
14
+ function readProjectPackage() {
15
+ try {
16
+ const pkgPath = path.join(cds.root, 'package.json')
17
+ if (!fs.existsSync(pkgPath)) return {}
18
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
19
+ } catch {
20
+ return {}
21
+ }
22
+ }
23
+
10
24
  // Auto-register plugin entity models based on service configuration.
11
25
  // See docs/feature-activation.md for reuse vs project-owned activation.
12
26
  const cachingEntries = getCachingRequiresEntries(cds.env.requires ?? {})
@@ -22,6 +36,9 @@ for (const root of pluginRoots) {
22
36
  }
23
37
  for (const message of warnings) LOG.warn(message)
24
38
 
39
+ const misplacedKeyManagement = detectMisplacedKeyManagement(cds.env, readProjectPackage())
40
+ if (misplacedKeyManagement) LOG.warn(misplacedKeyManagement)
41
+
25
42
  cds.on('served', scanCachingAnnotations)
26
43
 
27
44
  if (reuseDashboard) {
@@ -35,7 +52,16 @@ if (reuseDashboard) {
35
52
  );
36
53
  }
37
54
  cds.once('bootstrap', (app) => {
38
- app.use('/caching-dashboard', require('express').static(dashboardPath));
55
+ // Run CAP's middlewares first so cds.context.user is populated, then gate
56
+ // the static assets on it.
57
+ const { requireAuthenticatedUser, capRequestMiddlewares } = require('./lib/dashboard-guard');
58
+
59
+ app.use(
60
+ '/caching-dashboard',
61
+ ...capRequestMiddlewares(),
62
+ requireAuthenticatedUser,
63
+ require('express').static(dashboardPath)
64
+ );
39
65
  (app._app_links ??= []).push('/caching-dashboard');
40
66
  LOG.info("Serving cds-caching dashboard at /caching-dashboard");
41
67
  });
@@ -56,7 +82,6 @@ cds.build?.register?.('cds-caching', class CachingBuildPlugin extends cds.build.
56
82
  clean() { }
57
83
 
58
84
  static hasTask() {
59
- cds.log('cds-caching').info('hasTask', cds.env.requires);
60
85
  const requires = cds.env.requires || {};
61
86
  const dbKind = requires.db?.kind || '';
62
87
  const isHanaDB = dbKind === 'hana' || dbKind === 'sql';
package/index.cds CHANGED
@@ -2,13 +2,26 @@ using from './db/statistics';
2
2
 
3
3
  context plugin.cds_caching {
4
4
 
5
- @impl: 'cds-caching/srv/caching-api-service'
5
+ /**
6
+ * Management API for cache entries and metrics.
7
+ *
8
+ * Guarded with `authenticated-user` by default, because these operations can
9
+ * read and flush cache contents. Override in your own model to require a
10
+ * dedicated role instead:
11
+ *
12
+ * annotate plugin.cds_caching.CachingApiService with @requires: 'CacheAdmin';
13
+ */
14
+ @impl : 'cds-caching/srv/caching-api-service'
15
+ @requires: 'authenticated-user'
6
16
  service CachingApiService {
7
17
 
18
+ // Writes go through the bound actions below, never through CRUD, so that
19
+ // config rows (metricsEnabled, config) cannot be tampered with directly.
20
+ @readonly
8
21
  entity Caches as projection on plugin.cds_caching.Caches
9
22
  actions {
10
23
 
11
- function getEntries() returns array of {
24
+ function getEntries(top : Integer, skip : Integer) returns array of {
12
25
  entryKey : String;
13
26
  value : String;
14
27
  timestamp : DateTime;
@@ -23,6 +23,9 @@ class CachingService extends cds.Service {
23
23
  credentials: {},
24
24
  namespace: null,
25
25
  throwOnErrors: false,
26
+ // When enabled, read-through HTTP responses carry the full cache key in
27
+ // `x-sap-cap-cache-key`. Useful for debugging, unsafe in production.
28
+ debugHeaders: false,
26
29
  // When enabled, basic cache operations (`get`, `set`, `delete`, ...)
27
30
  // will be executed in a dedicated cache transaction (`cache.tx()`),
28
31
  // isolating them from the caller's request transaction (e.g. concurrent BEFORE handlers).
@@ -109,6 +109,7 @@ function getStatisticsHandlerOptions(metrics) {
109
109
  if (metrics.persistenceInterval != null) opts.persistenceInterval = metrics.persistenceInterval
110
110
  if (metrics.maxLatencies != null) opts.maxLatencies = metrics.maxLatencies
111
111
  if (metrics.maxKeyMetrics != null) opts.maxKeyMetrics = metrics.maxKeyMetrics
112
+ if (metrics.maxMetricFieldLength != null) opts.maxMetricFieldLength = metrics.maxMetricFieldLength
112
113
  if (metrics.enabled === true) opts.metricsEnabled = true
113
114
  if (metrics.keyMetricsEnabled === true) opts.keyMetricsEnabled = true
114
115
  return opts
@@ -122,10 +123,37 @@ function isMetricsConfigured(normalized) {
122
123
  return normalized.metrics != null
123
124
  }
124
125
 
126
+ /**
127
+ * Detect a `keyManagement` block placed outside `cds.requires.<cache>`.
128
+ *
129
+ * Releases up to 2.0.2 documented this as a top-level `"cds-caching"` key, which
130
+ * the runtime never read — so keys silently stayed context-free even though the
131
+ * project looked configured for user-aware caching. Warn loudly instead.
132
+ *
133
+ * @param {object} [env={}] - cds.env
134
+ * @param {object} [projectPackage={}] - Parsed project package.json
135
+ * @returns {string|null} Warning message, or null when nothing is misplaced
136
+ */
137
+ function detectMisplacedKeyManagement(env = {}, projectPackage = {}) {
138
+ const misplaced =
139
+ env['cds-caching']?.keyManagement ??
140
+ projectPackage['cds-caching']?.keyManagement
141
+
142
+ if (!misplaced) return null
143
+
144
+ return warnOnce(
145
+ 'misplaced-keyManagement',
146
+ 'cds-caching: a "cds-caching.keyManagement" block was found outside "cds.requires", where it is ignored. ' +
147
+ 'Cache keys are therefore NOT user/tenant/locale aware. Move it into the individual cache config, e.g. ' +
148
+ '{"cds":{"requires":{"caching":{"keyManagement":{"isUserAware":true}}}}}. See docs/key-management.md.'
149
+ )
150
+ }
151
+
125
152
  module.exports = {
126
153
  normalizeCachingConfig,
127
154
  getCachingRequiresEntries,
128
155
  getStatisticsHandlerOptions,
129
156
  isMetricsConfigured,
157
+ detectMisplacedKeyManagement,
130
158
  resetDeprecationWarnings,
131
159
  }
@@ -0,0 +1,32 @@
1
+ const cds = require('@sap/cds')
2
+
3
+ /**
4
+ * Express guard requiring an authenticated user.
5
+ *
6
+ * The dashboard is served as static files outside CAP's service adapters, so it
7
+ * inherits no authorization from the model. Restricting `CachingApiService` alone
8
+ * would leave this route open.
9
+ *
10
+ * @param {object} req - Express request
11
+ * @param {object} res - Express response
12
+ * @param {Function} next - Downstream handler
13
+ */
14
+ function requireAuthenticatedUser(req, res, next) {
15
+ const user = cds.context?.user
16
+ if (user?.is?.('authenticated-user')) return next()
17
+
18
+ res.status(401).set('WWW-Authenticate', 'Basic realm="cds-caching"').end()
19
+ }
20
+
21
+ /**
22
+ * CAP's own request middlewares (context, trace, auth, model), flattened and
23
+ * compacted so they can be spread into an `app.use()` call. These populate
24
+ * `cds.context.user`, which the guard above depends on.
25
+ *
26
+ * @returns {Function[]}
27
+ */
28
+ function capRequestMiddlewares() {
29
+ return (cds.middlewares?.before ?? []).flat().filter(Boolean)
30
+ }
31
+
32
+ module.exports = { requireAuthenticatedUser, capRequestMiddlewares }
@@ -1,5 +1,6 @@
1
1
  const TagResolver = require('../support/TagResolver');
2
2
  const telemetry = require('../support/Telemetry');
3
+ const { redactHeaders } = require('../support/metricsSanitizer');
3
4
  /**
4
5
  * Manages CAP-specific cache operations
5
6
  */
@@ -19,6 +20,16 @@ class CapOperations {
19
20
  this.log = log || console;
20
21
  }
21
22
 
23
+ /**
24
+ * Whether the full cache key may be echoed back in the `x-sap-cap-cache-key`
25
+ * response header. Off unless opted in, because the key is enough to target a
26
+ * specific cached entry from outside.
27
+ * @returns {boolean}
28
+ */
29
+ debugHeadersEnabled() {
30
+ return this.runtimeConfigManager?.options?.debugHeaders === true;
31
+ }
32
+
22
33
  /**
23
34
  * Safely execute cache operations with error handling
24
35
  * @param {Function} operation - The cache operation to execute
@@ -140,7 +151,7 @@ class CapOperations {
140
151
  subject: request.subject,
141
152
  query: request.query,
142
153
  event: request.event,
143
- headers: request.headers,
154
+ headers: redactHeaders(request.headers),
144
155
  }),
145
156
  cacheOptions: JSON.stringify(requestOptions)
146
157
  };
@@ -283,7 +294,9 @@ class CapOperations {
283
294
  req.cacheOptions = req.event ? this.extractFunctionCacheOptions(req, arguments[2]) : this.extractEntityCacheOptions(req, arguments[2]);
284
295
 
285
296
  req.cacheKey = this.keyManager.createKey(req, {}, req.cacheOptions.key);
286
- req.res?.setHeader('x-sap-cap-cache-key', req.cacheKey);
297
+ if (this.debugHeadersEnabled()) {
298
+ req.res?.setHeader('x-sap-cap-cache-key', req.cacheKey);
299
+ }
287
300
 
288
301
  // Track cache operation timing
289
302
  const startTime = process.hrtime();
@@ -4,6 +4,7 @@ const StatisticsPersistenceManager = require('./StatisticsPersistenceManager');
4
4
  const telemetry = require('./Telemetry');
5
5
  const { query } = require('@sap/cds/lib/env/defaults');
6
6
  const { isPluginModelAvailable } = require('../util');
7
+ const { truncateMetadataFields, MAX_FIELD_LENGTH } = require('./metricsSanitizer');
7
8
 
8
9
  /**
9
10
  * Class to handle all things caching statistics.
@@ -17,6 +18,7 @@ class CacheStatisticsHandler {
17
18
  persistenceInterval: 10 * 1000, // 10 seconds
18
19
  maxLatencies: 2000,
19
20
  maxKeyMetrics: 1000, // Track top accessed keys
21
+ maxMetricFieldLength: MAX_FIELD_LENGTH, // Cap per free-text KeyMetrics field
20
22
  keyMetricsEnabled: false,
21
23
  metricsEnabled: false, // Main metrics enabled flag
22
24
  ...options
@@ -418,6 +420,10 @@ class CacheStatisticsHandler {
418
420
  return;
419
421
  }
420
422
 
423
+ // Single choke point for every operation path, so no caller can persist
424
+ // an unbounded serialized request into KeyMetrics.
425
+ metadata = truncateMetadataFields(metadata, this.options.maxMetricFieldLength);
426
+
421
427
  if (!this.stats.current.keyAccess.has(key)) {
422
428
  this.log.debug(`Creating new key stats for: ${key}`);
423
429
  this.stats.current.keyAccess.set(key, {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Sanitizing helpers for data that gets persisted into the KeyMetrics entity.
3
+ *
4
+ * KeyMetrics rows are long-lived and readable through the management API, so
5
+ * credentials must never reach them and single fields must not grow unbounded.
6
+ */
7
+
8
+ /** Header names whose values are credentials or session material. */
9
+ const SENSITIVE_HEADERS = new Set([
10
+ 'authorization',
11
+ 'proxy-authorization',
12
+ 'cookie',
13
+ 'set-cookie',
14
+ 'x-api-key',
15
+ 'apikey',
16
+ 'x-csrf-token',
17
+ 'x-xsrf-token',
18
+ 'sap-passport',
19
+ ]);
20
+
21
+ const REDACTED = '[redacted]';
22
+
23
+ /** Default cap for free-text metric fields (metadata, query, subject, context). */
24
+ const MAX_FIELD_LENGTH = 8192;
25
+
26
+ /** Fields that carry serialized request/query detail and can grow without bound. */
27
+ const TRUNCATED_FIELDS = ['metadata', 'query', 'subject', 'context', 'cacheOptions'];
28
+
29
+ /**
30
+ * Copy headers with credential-bearing values replaced.
31
+ * @param {object} headers - Raw header map (case-insensitive keys)
32
+ * @returns {object|undefined} Redacted copy, or undefined if there was nothing to copy
33
+ */
34
+ function redactHeaders(headers) {
35
+ if (!headers || typeof headers !== 'object') return undefined;
36
+
37
+ const safe = {};
38
+ for (const [name, value] of Object.entries(headers)) {
39
+ safe[name] = SENSITIVE_HEADERS.has(name.toLowerCase()) ? REDACTED : value;
40
+ }
41
+ return safe;
42
+ }
43
+
44
+ /**
45
+ * Truncate a single field to `max` characters, marking where it was cut.
46
+ * @param {any} value - Field value; non-strings are returned unchanged
47
+ * @param {number} [max=MAX_FIELD_LENGTH] - Maximum length to keep
48
+ * @returns {any}
49
+ */
50
+ function truncateField(value, max = MAX_FIELD_LENGTH) {
51
+ if (typeof value !== 'string' || value.length <= max) return value;
52
+ return `${value.slice(0, max)}…[truncated ${value.length - max} chars]`;
53
+ }
54
+
55
+ /**
56
+ * Bound the free-text fields of a key-metrics metadata object.
57
+ * Returns the input unchanged when nothing needs truncating, so the common
58
+ * path allocates nothing.
59
+ * @param {object} metadata - Metadata as assembled by the operation layers
60
+ * @param {number} [max=MAX_FIELD_LENGTH] - Maximum length per field
61
+ * @returns {object}
62
+ */
63
+ function truncateMetadataFields(metadata, max = MAX_FIELD_LENGTH) {
64
+ if (!metadata || typeof metadata !== 'object') return metadata;
65
+
66
+ let copy = null;
67
+ for (const field of TRUNCATED_FIELDS) {
68
+ const value = metadata[field];
69
+ if (typeof value === 'string' && value.length > max) {
70
+ copy ??= { ...metadata };
71
+ copy[field] = truncateField(value, max);
72
+ }
73
+ }
74
+ return copy ?? metadata;
75
+ }
76
+
77
+ module.exports = {
78
+ SENSITIVE_HEADERS,
79
+ REDACTED,
80
+ MAX_FIELD_LENGTH,
81
+ redactHeaders,
82
+ truncateField,
83
+ truncateMetadataFields,
84
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cds-caching",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "A caching plugin for SAP CAP applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,13 @@ const { isPluginModelAvailable } = require('../lib/util')
4
4
 
5
5
  const DEFAULT_TENANT = '_default';
6
6
 
7
+ /** Upper bound for `getEntries`, regardless of the requested `top`. */
8
+ const MAX_ENTRIES_PER_PAGE = 1000;
9
+ const DEFAULT_ENTRIES_PER_PAGE = 100;
10
+
11
+ /** Upper bound for a single `setEntry` value, in bytes. */
12
+ const MAX_ENTRY_VALUE_BYTES = 1024 * 1024;
13
+
7
14
  class CachingApiService extends cds.ApplicationService {
8
15
  log = cds.log('cds-caching');
9
16
 
@@ -20,8 +27,8 @@ class CachingApiService extends cds.ApplicationService {
20
27
  // Handle setMetricsEnabled action
21
28
  this.on('setMetricsEnabled', async (req) => {
22
29
  const { enabled } = req.data
23
- const cache = req.params[0].name;
24
- const cacheService = await cds.connect.to(cache);
30
+ const cacheService = await this._connectToCache(req);
31
+ const cache = this._cacheName(req);
25
32
  try {
26
33
  await cacheService.setMetricsEnabled(enabled)
27
34
  req.info(`Metrics ${enabled ? 'enabled' : 'disabled'} for cache ${cache}`);
@@ -35,8 +42,8 @@ class CachingApiService extends cds.ApplicationService {
35
42
  // Handle setKeyMetricsEnabled action
36
43
  this.on('setKeyMetricsEnabled', async (req) => {
37
44
  const { enabled } = req.data
38
- const cache = req.params[0].name;
39
- const cacheService = await cds.connect.to(cache);
45
+ const cacheService = await this._connectToCache(req);
46
+ const cache = this._cacheName(req);
40
47
  try {
41
48
  await cacheService.setKeyMetricsEnabled(enabled)
42
49
  req.info(`Key metrics ${enabled ? 'enabled' : 'disabled'} for cache ${cache}`);
@@ -49,17 +56,27 @@ class CachingApiService extends cds.ApplicationService {
49
56
 
50
57
  // Handle getCacheEntries function
51
58
  this.on('getEntries', async (req) => {
52
- const cache = req.params[0].name;
59
+ const cacheService = await this._connectToCache(req);
60
+ const { top, skip } = req.data;
61
+
62
+ // Bounded on purpose: an unbounded iteration would pull the whole
63
+ // cache into memory and can be used to exhaust the server.
64
+ const requested = Number.isInteger(top) && top > 0 ? top : DEFAULT_ENTRIES_PER_PAGE;
65
+ const limit = Math.min(requested, MAX_ENTRIES_PER_PAGE);
66
+ const offset = Number.isInteger(skip) && skip > 0 ? skip : 0;
67
+
53
68
  try {
54
- const cacheService = await cds.connect.to(cache);
55
69
  const entries = [];
70
+ let seen = 0;
56
71
  for await (const [key, value] of cacheService.iterator()) {
72
+ if (seen++ < offset) continue;
57
73
  entries.push({
58
74
  entryKey: key,
59
75
  value: JSON.stringify(value.value),
60
76
  timestamp: value.timestamp,
61
77
  tags: value.tags,
62
78
  });
79
+ if (entries.length >= limit) break;
63
80
  }
64
81
  return entries;
65
82
  } catch (error) {
@@ -71,8 +88,7 @@ class CachingApiService extends cds.ApplicationService {
71
88
  // Handle getCacheEntry function
72
89
  this.on('getEntry', async (req) => {
73
90
  const { key } = req.data;
74
- const cache = req.params[0].name;
75
- const cacheService = await cds.connect.to(cache);
91
+ const cacheService = await this._connectToCache(req);
76
92
  const value = await cacheService.get(key);
77
93
  return {
78
94
  value: value,
@@ -82,8 +98,13 @@ class CachingApiService extends cds.ApplicationService {
82
98
  // Handle setCacheEntry action
83
99
  this.on('setEntry', async (req) => {
84
100
  const { key, value, ttl } = req.data;
85
- const cache = req.params[0].name;
86
- const cacheService = await cds.connect.to(cache);
101
+ const cacheService = await this._connectToCache(req);
102
+
103
+ const size = Buffer.byteLength(String(value ?? ''), 'utf8');
104
+ if (size > MAX_ENTRY_VALUE_BYTES) {
105
+ return req.reject(400, `Value exceeds the maximum of ${MAX_ENTRY_VALUE_BYTES} bytes (got ${size})`);
106
+ }
107
+
87
108
  await cacheService.set(key, value, { ttl: ttl });
88
109
  req.info(`Cache entry set successfully: ${key}`);
89
110
  return true;
@@ -92,8 +113,7 @@ class CachingApiService extends cds.ApplicationService {
92
113
  // Handle deleteCacheEntry action
93
114
  this.on('deleteEntry', async (req) => {
94
115
  const { key } = req.data;
95
- const cache = req.params[0].name;
96
- const cacheService = await cds.connect.to(cache);
116
+ const cacheService = await this._connectToCache(req);
97
117
  await cacheService.delete(key);
98
118
  req.info(`Cache entry deleted successfully: ${key}`);
99
119
  return true;
@@ -101,8 +121,8 @@ class CachingApiService extends cds.ApplicationService {
101
121
 
102
122
  // Handle clearCache action
103
123
  this.on('clear', async (req) => {
104
- const cache = req.params[0].name;
105
- const cacheService = await cds.connect.to(cache);
124
+ const cacheService = await this._connectToCache(req);
125
+ const cache = this._cacheName(req);
106
126
  await cacheService.clear();
107
127
  req.info(`Cache cleared successfully: ${cache}`);
108
128
  return true;
@@ -110,8 +130,8 @@ class CachingApiService extends cds.ApplicationService {
110
130
 
111
131
  // Handle clearKeyMetrics action
112
132
  this.on('clearKeyMetrics', async (req) => {
113
- const cache = req.params[0].name;
114
- const cacheService = await cds.connect.to(cache);
133
+ const cacheService = await this._connectToCache(req);
134
+ const cache = this._cacheName(req);
115
135
  await cacheService.clearKeyMetrics();
116
136
  req.info(`Key metrics cleared successfully: ${cache}`);
117
137
  return true;
@@ -119,8 +139,8 @@ class CachingApiService extends cds.ApplicationService {
119
139
 
120
140
  // Handle clearMetrics action
121
141
  this.on('clearMetrics', async (req) => {
122
- const cache = req.params[0].name;
123
- const cacheService = await cds.connect.to(cache);
142
+ const cacheService = await this._connectToCache(req);
143
+ const cache = this._cacheName(req);
124
144
  await cacheService.clearMetrics();
125
145
  req.info(`Metrics cleared successfully: ${cache}`);
126
146
  return true;
@@ -129,6 +149,46 @@ class CachingApiService extends cds.ApplicationService {
129
149
  await super.init()
130
150
  }
131
151
 
152
+ /**
153
+ * Names of all configured cds-caching services. Used as an allow-list so a
154
+ * caller cannot coerce `cds.connect.to()` into connecting to an unrelated
155
+ * business service by passing its name as the cache key.
156
+ * @returns {Set<string>}
157
+ */
158
+ _allowedCaches() {
159
+ return new Set(
160
+ Object.entries(cds.env.requires || {})
161
+ .filter(([, config]) => config?.impl === 'cds-caching')
162
+ .map(([name]) => name)
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Name of the cache addressed by the request key.
168
+ * @param {object} req - Inbound request carrying the Caches key
169
+ * @returns {string|undefined}
170
+ */
171
+ _cacheName(req) {
172
+ const param = req.params?.[0];
173
+ return typeof param === 'string' ? param : param?.name;
174
+ }
175
+
176
+ /**
177
+ * Resolve the cache addressed by the request key and connect to it.
178
+ * Rejects with 404 for anything that is not a configured cache.
179
+ * @param {object} req - Inbound request carrying the Caches key
180
+ * @returns {Promise<object>} The connected CachingService
181
+ */
182
+ async _connectToCache(req) {
183
+ const name = this._cacheName(req);
184
+
185
+ if (!name || !this._allowedCaches().has(name)) {
186
+ return req.reject(404, `Unknown cache: ${name}`);
187
+ }
188
+
189
+ return cds.connect.to(name);
190
+ }
191
+
132
192
  /**
133
193
  * Ensure cache entries exist in the current tenant's DB.
134
194
  * Called lazily in MTX mode on first dashboard access.