@sap-ux/backend-proxy-middleware-cf 0.0.98 → 0.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.
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadAndApplyEnvOptions = loadAndApplyEnvOptions;
7
+ exports.updateUi5ServerDestinationPort = updateUi5ServerDestinationPort;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const adp_tooling_1 = require("@sap-ux/adp-tooling");
11
+ const constants_1 = require("./constants");
12
+ /**
13
+ * Load and parse env options JSON file.
14
+ *
15
+ * @param rootPath - Project root path.
16
+ * @param envOptionsPath - Path to file (relative to rootPath).
17
+ * @returns Parsed options object.
18
+ */
19
+ function loadEnvOptionsFromFile(rootPath, envOptionsPath) {
20
+ const resolvedPath = node_path_1.default.resolve(rootPath, envOptionsPath);
21
+ if (!node_fs_1.default.existsSync(resolvedPath)) {
22
+ throw new Error(`Env options file not found at "${resolvedPath}" (envOptionsPath: "${envOptionsPath}").`);
23
+ }
24
+ try {
25
+ const content = node_fs_1.default.readFileSync(resolvedPath, 'utf8');
26
+ return JSON.parse(content);
27
+ }
28
+ catch (err) {
29
+ const message = err instanceof Error ? err.message : String(err);
30
+ throw new Error(`Failed to read env options from "${resolvedPath}": ${message}.`);
31
+ }
32
+ }
33
+ /**
34
+ * Apply options to process.env with JSON-stringified destinations and VCAP_SERVICES.
35
+ *
36
+ * @param options - Env options to apply.
37
+ */
38
+ function applyToProcessEnv(options) {
39
+ const envOptions = {
40
+ ...options,
41
+ ...(options.destinations ? { destinations: JSON.stringify(options.destinations) } : {}),
42
+ ...(options.VCAP_SERVICES ? { VCAP_SERVICES: JSON.stringify(options.VCAP_SERVICES) } : {})
43
+ };
44
+ Object.assign(process.env, envOptions);
45
+ }
46
+ /**
47
+ * Load env options from file or CF, apply to process.env, and add destinations from effectiveOptions.
48
+ *
49
+ * When effectiveOptions.envOptionsPath is set, loads that JSON file. When null, loads mta.yaml one level
50
+ * above rootPath and fetches VCAP_SERVICES from CF. effectiveOptions.destinations is applied so
51
+ * middleware config takes precedence over file/env.
52
+ *
53
+ * @param rootPath - Project root path.
54
+ * @param effectiveOptions - Merged config; envOptionsPath and destinations are used.
55
+ * @param logger - Logger for CF path.
56
+ * @returns Promise resolving when env options are loaded and applied.
57
+ */
58
+ async function loadAndApplyEnvOptions(rootPath, effectiveOptions, logger) {
59
+ const { envOptionsPath, destinations: middlewareDestinations } = effectiveOptions;
60
+ let options;
61
+ if (envOptionsPath) {
62
+ const envOptions = loadEnvOptionsFromFile(rootPath, envOptionsPath);
63
+ const destinations = envOptions.destinations
64
+ ? [...envOptions.destinations, ...middlewareDestinations]
65
+ : middlewareDestinations;
66
+ options = { ...envOptions, destinations };
67
+ }
68
+ else {
69
+ const mtaPath = node_path_1.default.resolve(rootPath, '..', 'mta.yaml');
70
+ const spaceGuid = await (0, adp_tooling_1.getSpaceGuidFromUi5Yaml)(rootPath, logger);
71
+ if (!spaceGuid) {
72
+ throw new Error('No space GUID (from config or ui5.yaml). Cannot load CF env options.');
73
+ }
74
+ if (!node_fs_1.default.existsSync(mtaPath)) {
75
+ throw new Error(`mta.yaml not found at "${mtaPath}". Cannot load CF env options.`);
76
+ }
77
+ const mtaYaml = (0, adp_tooling_1.getYamlContent)(mtaPath);
78
+ const VCAP_SERVICES = await (0, adp_tooling_1.buildVcapServicesFromResources)(mtaYaml.resources, spaceGuid, logger);
79
+ options = {
80
+ VCAP_SERVICES,
81
+ destinations: middlewareDestinations
82
+ };
83
+ }
84
+ applyToProcessEnv(options);
85
+ }
86
+ /**
87
+ * Ensure the ui5-server destination exists and has the correct URL.
88
+ * If ui5-server doesn't exist in configuration, it will be auto-created.
89
+ * If it exists but has a different port, it will be updated.
90
+ * In BAS, the external URL is used instead of localhost so the approuter
91
+ * builds correct redirect URIs.
92
+ *
93
+ * This enables multi-instance support and removes the need to manually
94
+ * configure ui5-server in ui5.yaml - it's auto-configured based on the actual port.
95
+ *
96
+ * @param effectiveOptions - Merged options containing destinations.
97
+ * @param actualPort - The actual port detected from the incoming request.
98
+ * @param basExternalUrl - Optional BAS external URL; when set, used as the destination URL.
99
+ * @returns True if destination was created or updated, false if no change needed.
100
+ */
101
+ function updateUi5ServerDestinationPort(effectiveOptions, actualPort, basExternalUrl) {
102
+ const newUrl = basExternalUrl ? basExternalUrl.href : `http://localhost:${actualPort}`;
103
+ let ui5ServerDest = effectiveOptions.destinations.find((d) => d.name === constants_1.UI5_SERVER_DESTINATION);
104
+ if (!ui5ServerDest) {
105
+ ui5ServerDest = { name: constants_1.UI5_SERVER_DESTINATION, url: newUrl };
106
+ effectiveOptions.destinations.push(ui5ServerDest);
107
+ const envDestinations = JSON.parse(process.env.destinations ?? '[]');
108
+ envDestinations.push({ name: constants_1.UI5_SERVER_DESTINATION, url: newUrl });
109
+ process.env.destinations = JSON.stringify(envDestinations);
110
+ return true;
111
+ }
112
+ const currentUrl = new URL(ui5ServerDest.url);
113
+ const currentPort = Number.parseInt(currentUrl.port, 10) || 80;
114
+ if (currentPort === actualPort) {
115
+ return false;
116
+ }
117
+ ui5ServerDest.url = newUrl;
118
+ const envDestinations = JSON.parse(process.env.destinations ?? '[]');
119
+ const envUi5ServerDest = envDestinations.find((d) => d.name === constants_1.UI5_SERVER_DESTINATION);
120
+ if (envUi5ServerDest) {
121
+ envUi5ServerDest.url = newUrl;
122
+ }
123
+ else {
124
+ envDestinations.push({ name: constants_1.UI5_SERVER_DESTINATION, url: newUrl });
125
+ }
126
+ process.env.destinations = JSON.stringify(envDestinations);
127
+ return true;
128
+ }
129
+ //# sourceMappingURL=env.js.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { CfOAuthMiddlewareConfig } from './types';
1
+ export type { BackendProxyMiddlewareCfConfig, ApprouterDestination, ApprouterExtension } from './types';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1,34 +1,101 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_fs_1 = __importDefault(require("node:fs"));
7
+ const dotenv_1 = __importDefault(require("dotenv"));
8
+ const node_path_1 = __importDefault(require("node:path"));
3
9
  const logger_1 = require("@sap-ux/logger");
4
- const proxy_1 = require("./proxy");
5
- const validation_1 = require("./validation");
6
- const token_1 = require("./token");
10
+ const utils_1 = require("./utils");
11
+ const proxy_1 = require("./proxy/proxy");
12
+ const approuter_1 = require("./approuter/approuter");
13
+ const extensions_1 = require("./approuter/extensions");
14
+ const config_1 = require("./config/config");
15
+ const xssecurity_1 = require("./platform/xssecurity");
16
+ const bas_1 = require("./platform/bas");
17
+ const routes_1 = require("./proxy/routes");
18
+ const env_1 = require("./config/env");
19
+ dotenv_1.default.config();
7
20
  /**
8
- * UI5 middleware for proxying requests to Cloud Foundry destinations with OAuth2 authentication.
9
- * Supports multiple destination URLs with their own OData source paths.
21
+ * UI5 server middleware: runs `@sap/approuter` and proxies matching requests to it.
22
+ * Uses lazy initialization to detect the actual UI5 server port from the first request,
23
+ * enabling multi-instance support where hardcoded ports in ui5.yaml may differ from runtime.
10
24
  *
11
- * @param {MiddlewareParameters<CfOAuthMiddlewareConfig>} params - Input parameters for UI5 middleware.
12
- * @param {CfOAuthMiddlewareConfig} params.options - Configuration options.
13
- * @returns {Promise<RequestHandler>} Express middleware handler.
25
+ * @param params - Middleware parameters from UI5 (options, middlewareUtil).
26
+ * @param params.options - Options containing configuration from ui5.yaml.
27
+ * @param params.middlewareUtil - UI5 middleware utilities (getProject, etc.).
28
+ * @returns Promise resolving to the proxy request handler.
14
29
  */
15
- module.exports = async ({ options }) => {
16
- const config = options.configuration;
17
- if (!config) {
30
+ async function backendProxyMiddlewareCf({ options, middlewareUtil }) {
31
+ const configuration = options.configuration;
32
+ if (!configuration) {
18
33
  throw new Error('Backend proxy middleware (CF) has no configuration.');
19
34
  }
20
35
  const logger = new logger_1.ToolsLogger({
21
- logLevel: config.debug ? logger_1.LogLevel.Debug : logger_1.LogLevel.Info,
36
+ logLevel: configuration.debug ? logger_1.LogLevel.Debug : logger_1.LogLevel.Info,
22
37
  transports: [new logger_1.UI5ToolingTransport({ moduleName: 'backend-proxy-middleware-cf' })]
23
38
  });
24
- await (0, validation_1.validateConfig)(config, logger);
25
- const tokenProvider = await (0, token_1.createTokenProvider)(config, logger);
26
- // Setup proxy routes for all backends
27
- const router = (0, proxy_1.setupProxyRoutes)(config.backends, tokenProvider, logger);
28
- // Log initialization
29
- config.backends.forEach((backend) => {
30
- logger.info(`Backend proxy middleware (CF) initialized: url=${backend.url}, paths=${backend.paths.join(', ')}`);
39
+ const effectiveOptions = (0, config_1.mergeEffectiveOptions)(configuration);
40
+ process.env.WS_ALLOWED_ORIGINS = process.env.WS_ALLOWED_ORIGINS ?? JSON.stringify([{ host: 'localhost' }]);
41
+ process.env.XS_APP_LOG_LEVEL = process.env.XS_APP_LOG_LEVEL ?? (effectiveOptions.debug ? 'DEBUG' : 'ERROR');
42
+ const project = middlewareUtil.getProject();
43
+ const rootPath = project.getRootPath() ?? process.cwd();
44
+ const xsappJsonPath = node_path_1.default.resolve(rootPath, effectiveOptions.xsappJsonPath);
45
+ if (!node_fs_1.default.existsSync(xsappJsonPath)) {
46
+ throw new Error(`xs-app.json not found at "${xsappJsonPath}"`);
47
+ }
48
+ await (0, env_1.loadAndApplyEnvOptions)(rootPath, effectiveOptions, logger);
49
+ await (0, xssecurity_1.updateXsuaaService)(rootPath, logger);
50
+ const sourcePath = project.getSourcePath();
51
+ const xsappConfig = (0, routes_1.loadAndPrepareXsappConfig)({
52
+ rootPath,
53
+ xsappJsonPath,
54
+ effectiveOptions,
55
+ sourcePath,
56
+ logger
31
57
  });
32
- return router;
33
- };
58
+ const { modules, routes: extensionsRoutes } = (0, extensions_1.loadExtensions)(rootPath, effectiveOptions.extensions, logger);
59
+ const port = await (0, utils_1.nextFreePort)(effectiveOptions.port, logger);
60
+ if (port !== effectiveOptions.port) {
61
+ logger.info(`Port ${effectiveOptions.port} already in use. Using next free port: ${port} for the AppRouter.`);
62
+ }
63
+ const subdomain = effectiveOptions.subdomain;
64
+ const baseUri = subdomain ? `http://${subdomain}.localhost:${port}` : `http://localhost:${port}`;
65
+ const callbackEndpoint = xsappConfig.login?.callbackEndpoint ?? '/login/callback';
66
+ const customRoutes = [...extensionsRoutes, callbackEndpoint];
67
+ if (!effectiveOptions.disableWelcomeFile) {
68
+ customRoutes.unshift('/');
69
+ }
70
+ const logoutEndpoint = xsappConfig.logout?.logoutEndpoint;
71
+ if (logoutEndpoint) {
72
+ customRoutes.push(logoutEndpoint);
73
+ }
74
+ const basUrlTemplate = await (0, bas_1.fetchBasUrlTemplate)(logger);
75
+ let initialized = false;
76
+ let proxyMiddleware = null;
77
+ return function lazyApprouterMiddleware(req, res, next) {
78
+ if (!initialized) {
79
+ try {
80
+ const actualPort = req.socket.localPort ?? 8080;
81
+ const basExternalUrl = (0, bas_1.resolveBasExternalUrl)(basUrlTemplate, actualPort);
82
+ if (basExternalUrl) {
83
+ logger.info(`BAS detected. External URL: ${basExternalUrl.href}`);
84
+ }
85
+ if ((0, env_1.updateUi5ServerDestinationPort)(effectiveOptions, actualPort, basExternalUrl)) {
86
+ logger.info(`Auto-configured ui5-server destination to port ${actualPort}`);
87
+ }
88
+ const routes = (0, routes_1.buildRouteEntries)({ xsappConfig, effectiveOptions, logger });
89
+ (0, approuter_1.startApprouter)({ port, xsappConfig, rootPath, modules, logger });
90
+ proxyMiddleware = (0, proxy_1.createProxy)({ customRoutes, routes, baseUri, effectiveOptions, basExternalUrl }, logger);
91
+ initialized = true;
92
+ }
93
+ catch (err) {
94
+ return next(err);
95
+ }
96
+ }
97
+ proxyMiddleware(req, res, next);
98
+ };
99
+ }
100
+ module.exports = backendProxyMiddlewareCf;
34
101
  //# sourceMappingURL=middleware.js.map
@@ -0,0 +1,19 @@
1
+ import type { ToolsLogger } from '@sap-ux/logger';
2
+ /**
3
+ * If running in BAS, fetch a URL template from the AppStudio API using a placeholder port.
4
+ * The template can later be resolved to the real port with {@link resolveBasExternalUrl}.
5
+ *
6
+ * @param logger - Logger instance.
7
+ * @returns URL template string, or empty string when not in BAS.
8
+ */
9
+ export declare function fetchBasUrlTemplate(logger: ToolsLogger): Promise<string>;
10
+ /**
11
+ * Replace the placeholder port in a BAS URL template with the actual runtime port
12
+ * and register the resulting hostname in `WS_ALLOWED_ORIGINS`.
13
+ *
14
+ * @param basUrlTemplate - Template URL from {@link fetchBasUrlTemplate}.
15
+ * @param actualPort - The real UI5 server port detected at runtime.
16
+ * @returns Resolved URL, or undefined if the template is empty.
17
+ */
18
+ export declare function resolveBasExternalUrl(basUrlTemplate: string, actualPort: number): URL | undefined;
19
+ //# sourceMappingURL=bas.d.ts.map
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchBasUrlTemplate = fetchBasUrlTemplate;
4
+ exports.resolveBasExternalUrl = resolveBasExternalUrl;
5
+ const btp_utils_1 = require("@sap-ux/btp-utils");
6
+ /** Port placeholder used to obtain a BAS URL template before the actual port is known. */
7
+ const BAS_PORT_PLACEHOLDER = 0;
8
+ /**
9
+ * If running in BAS, fetch a URL template from the AppStudio API using a placeholder port.
10
+ * The template can later be resolved to the real port with {@link resolveBasExternalUrl}.
11
+ *
12
+ * @param logger - Logger instance.
13
+ * @returns URL template string, or empty string when not in BAS.
14
+ */
15
+ async function fetchBasUrlTemplate(logger) {
16
+ if (!(0, btp_utils_1.isAppStudio)()) {
17
+ return '';
18
+ }
19
+ return (0, btp_utils_1.exposePort)(BAS_PORT_PLACEHOLDER, logger);
20
+ }
21
+ /**
22
+ * Replace the placeholder port in a BAS URL template with the actual runtime port
23
+ * and register the resulting hostname in `WS_ALLOWED_ORIGINS`.
24
+ *
25
+ * @param basUrlTemplate - Template URL from {@link fetchBasUrlTemplate}.
26
+ * @param actualPort - The real UI5 server port detected at runtime.
27
+ * @returns Resolved URL, or undefined if the template is empty.
28
+ */
29
+ function resolveBasExternalUrl(basUrlTemplate, actualPort) {
30
+ if (!basUrlTemplate) {
31
+ return undefined;
32
+ }
33
+ const basExternalUrl = new URL(basUrlTemplate.replace(`port${BAS_PORT_PLACEHOLDER}`, `port${actualPort}`));
34
+ const origins = JSON.parse(process.env.WS_ALLOWED_ORIGINS ?? '[]');
35
+ origins.push({ host: basExternalUrl.hostname });
36
+ process.env.WS_ALLOWED_ORIGINS = JSON.stringify(origins);
37
+ return basExternalUrl;
38
+ }
39
+ //# sourceMappingURL=bas.js.map
@@ -0,0 +1,10 @@
1
+ import type { ToolsLogger } from '@sap-ux/logger';
2
+ /**
3
+ * Update the XSUAA service instance with oauth2-configuration redirect-uris
4
+ * so that OAuth redirects work correctly in the BAS environment.
5
+ *
6
+ * @param rootPath - Project root path (app folder; mta.yaml and xs-security.json are one level up).
7
+ * @param logger - Logger instance.
8
+ */
9
+ export declare function updateXsuaaService(rootPath: string, logger: ToolsLogger): Promise<void>;
10
+ //# sourceMappingURL=xssecurity.d.ts.map
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateXsuaaService = updateXsuaaService;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const adp_tooling_1 = require("@sap-ux/adp-tooling");
10
+ /**
11
+ * Update the XSUAA service instance with oauth2-configuration redirect-uris
12
+ * so that OAuth redirects work correctly in the BAS environment.
13
+ *
14
+ * @param rootPath - Project root path (app folder; mta.yaml and xs-security.json are one level up).
15
+ * @param logger - Logger instance.
16
+ */
17
+ async function updateXsuaaService(rootPath, logger) {
18
+ try {
19
+ const projectRoot = node_path_1.default.resolve(rootPath, '..');
20
+ const xsSecurityPath = node_path_1.default.resolve(projectRoot, 'xs-security.json');
21
+ const mtaPath = node_path_1.default.resolve(projectRoot, 'mta.yaml');
22
+ if (!node_fs_1.default.existsSync(xsSecurityPath)) {
23
+ logger.warn(`xs-security.json not found at "${xsSecurityPath}", skipping XSUAA service update.`);
24
+ return;
25
+ }
26
+ if (!node_fs_1.default.existsSync(mtaPath)) {
27
+ logger.warn(`mta.yaml not found at "${mtaPath}", skipping XSUAA service update.`);
28
+ return;
29
+ }
30
+ const xsSecurityContent = JSON.parse(node_fs_1.default.readFileSync(xsSecurityPath, 'utf-8'));
31
+ const augmented = {
32
+ ...xsSecurityContent,
33
+ 'oauth2-configuration': {
34
+ 'redirect-uris': ['https://**.applicationstudio.cloud.sap/**', 'http://localhost:*/**']
35
+ }
36
+ };
37
+ const mtaServices = (0, adp_tooling_1.getServicesForFile)(mtaPath, logger);
38
+ const serviceInstanceName = mtaServices.find((s) => s.label === 'xsuaa')?.name;
39
+ if (!serviceInstanceName) {
40
+ logger.warn('No xsuaa service instance name found in mta.yaml, skipping XSUAA service update.');
41
+ return;
42
+ }
43
+ logger.info(`Updating XSUAA service instance "${serviceInstanceName}" with BAS redirect-uris.`);
44
+ await (0, adp_tooling_1.updateServiceInstance)(serviceInstanceName, augmented);
45
+ logger.info(`XSUAA service instance "${serviceInstanceName}" updated successfully.`);
46
+ }
47
+ catch (e) {
48
+ logger.error(`Failed to update XSUAA service instance for BAS: ${e.message}`);
49
+ }
50
+ }
51
+ //# sourceMappingURL=xssecurity.js.map
@@ -0,0 +1,22 @@
1
+ import type { RequestHandler } from 'express';
2
+ import { responseInterceptor } from 'http-proxy-middleware';
3
+ import type { ToolsLogger } from '@sap-ux/logger';
4
+ import type { CreateProxyOptions, EffectiveOptions, RouteEntry } from '../types';
5
+ /**
6
+ * Create the response interceptor for the proxy (content-type + URL rewriting).
7
+ *
8
+ * @param routes - Route entries with regex and destination URLs.
9
+ * @param effectiveOptions - Merged options (rewriteContent, rewriteContentTypes, debug).
10
+ * @returns The interceptor function to pass to responseInterceptor().
11
+ */
12
+ export declare function createResponseInterceptor(routes: RouteEntry[], effectiveOptions: EffectiveOptions): ReturnType<typeof responseInterceptor>;
13
+ /**
14
+ * Create the proxy middleware that forwards matching requests to the approuter.
15
+ * Paths are proxied if they match any customRoute (e.g. welcome, login callback) or any destination route.
16
+ *
17
+ * @param options - customRoutes, routes, baseUri, effectiveOptions.
18
+ * @param logger - Logger instance.
19
+ * @returns Express request handler (the proxy middleware).
20
+ */
21
+ export declare function createProxy(options: CreateProxyOptions, logger: ToolsLogger): RequestHandler;
22
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createResponseInterceptor = createResponseInterceptor;
4
+ exports.createProxy = createProxy;
5
+ const http_proxy_middleware_1 = require("http-proxy-middleware");
6
+ const constants_1 = require("../config/constants");
7
+ const utils_1 = require("./utils");
8
+ /**
9
+ * Create the response interceptor for the proxy (content-type + URL rewriting).
10
+ *
11
+ * @param routes - Route entries with regex and destination URLs.
12
+ * @param effectiveOptions - Merged options (rewriteContent, rewriteContentTypes, debug).
13
+ * @returns The interceptor function to pass to responseInterceptor().
14
+ */
15
+ function createResponseInterceptor(routes, effectiveOptions) {
16
+ return (0, http_proxy_middleware_1.responseInterceptor)(async (responseBuffer, proxyRes, req, res) => {
17
+ const url = req.url ?? '';
18
+ const pathname = /^[^?]*/.exec(url)?.[0] ?? url;
19
+ const { type, charset, contentType: ct } = (0, utils_1.getMimeInfo)(pathname, proxyRes.headers['content-type']);
20
+ res.setHeader('content-type', ct);
21
+ const route = routes.find((routeEntry) => routeEntry.sourcePattern.test(url));
22
+ if (route?.path && route.url && effectiveOptions?.rewriteContentTypes?.includes(type?.toLowerCase() ?? '')) {
23
+ const encoding = (charset ?? 'utf8');
24
+ let data = responseBuffer.toString(encoding);
25
+ const referrer = req.headers.referrer ?? req.headers.referer ?? (0, utils_1.getRequestOrigin)(req);
26
+ const referrerUrl = new URL(route.path, referrer).toString();
27
+ const routeUrlParsed = new URL(route.url);
28
+ const hostAndPath = `${routeUrlParsed.host}${routeUrlParsed.pathname}`;
29
+ data = (0, utils_1.replaceUrl)(data, `https://${hostAndPath}`, referrerUrl);
30
+ data = (0, utils_1.replaceUrl)(data, `http://${hostAndPath}`, referrerUrl);
31
+ return Buffer.from(data);
32
+ }
33
+ return responseBuffer;
34
+ });
35
+ }
36
+ /**
37
+ * Create the proxy middleware that forwards matching requests to the approuter.
38
+ * Paths are proxied if they match any customRoute (e.g. welcome, login callback) or any destination route.
39
+ *
40
+ * @param options - customRoutes, routes, baseUri, effectiveOptions.
41
+ * @param logger - Logger instance.
42
+ * @returns Express request handler (the proxy middleware).
43
+ */
44
+ function createProxy(options, logger) {
45
+ const { customRoutes, routes, baseUri, effectiveOptions, basExternalUrl } = options;
46
+ const intercept = createResponseInterceptor(routes, effectiveOptions);
47
+ const proxyFilter = (0, utils_1.createProxyFilter)(customRoutes, routes);
48
+ const proxyMiddleware = (0, http_proxy_middleware_1.createProxyMiddleware)({
49
+ logger: effectiveOptions.debug ? logger : undefined,
50
+ target: baseUri,
51
+ pathFilter: proxyFilter,
52
+ changeOrigin: true,
53
+ selfHandleResponse: true,
54
+ autoRewrite: true,
55
+ xfwd: true,
56
+ on: {
57
+ proxyReq: (proxyReq, req, res) => {
58
+ proxyReq.setHeader(constants_1.PROXY_MARKER_HEADER, '1');
59
+ const xfp = req.headers['x-forwarded-proto'];
60
+ if (typeof xfp === 'string' && xfp.includes(',')) {
61
+ const proto = xfp.split(',')[0];
62
+ req.headers['x-forwarded-proto'] = proto;
63
+ proxyReq.setHeader('x-forwarded-proto', proto);
64
+ }
65
+ if (basExternalUrl) {
66
+ proxyReq.setHeader('x-forwarded-host', basExternalUrl.host);
67
+ proxyReq.setHeader('x-forwarded-proto', basExternalUrl.protocol.replace(':', ''));
68
+ }
69
+ if (req['ui5-middleware-index']?.url === '/') {
70
+ res['backend-proxy-middleware-cf'] = { redirected: true };
71
+ const baseUrl = req['ui5-patched-router']?.baseUrl ?? '/';
72
+ res.redirect(`${baseUrl === '/' ? '' : baseUrl}${req.url ?? ''}`);
73
+ }
74
+ else {
75
+ const originalUrl = req['ui5-patched-router']?.originalUrl;
76
+ if (originalUrl) {
77
+ proxyReq.setHeader('x-forwarded-path', originalUrl);
78
+ }
79
+ }
80
+ },
81
+ proxyRes: async (proxyRes, req, res) => {
82
+ if (!res['backend-proxy-middleware-cf']?.redirected) {
83
+ return intercept(proxyRes, req, res);
84
+ }
85
+ return undefined;
86
+ },
87
+ error: (err, _req, res) => {
88
+ logger.error(`Approuter proxy error: ${err.message}`);
89
+ const response = res;
90
+ if (!response.headersSent) {
91
+ response.writeHead(502, { 'Content-Type': 'text/plain' });
92
+ response.end(`Approuter is not reachable: ${err.message}`);
93
+ }
94
+ }
95
+ }
96
+ });
97
+ return proxyMiddleware;
98
+ }
99
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1,18 @@
1
+ import type { BuildRouteEntriesOptions, PrepareXsappConfigOptions, RouteEntry, XsappConfig } from '../types';
2
+ /**
3
+ * Load xs-app.json and prepare it for the approuter (filter routes, set auth, optionally append auth route).
4
+ * Mutates and returns the config; does not build RouteEntry[].
5
+ *
6
+ * @param options - rootPath, xsappJsonPath, effectiveOptions, sourcePath.
7
+ * @returns The loaded and mutated XsappConfig.
8
+ */
9
+ export declare function loadAndPrepareXsappConfig(options: PrepareXsappConfigOptions): XsappConfig;
10
+ /**
11
+ * Build the list of route entries (compiled regex + resolved destination URLs) from a prepared xsappConfig.
12
+ * Does not read files or mutate xsappConfig.
13
+ *
14
+ * @param options - xsappConfig, effectiveOptions, logger.
15
+ * @returns Route entries for the proxy.
16
+ */
17
+ export declare function buildRouteEntries(options: BuildRouteEntriesOptions): RouteEntry[];
18
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadAndPrepareXsappConfig = loadAndPrepareXsappConfig;
7
+ exports.buildRouteEntries = buildRouteEntries;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const constants_1 = require("../config/constants");
11
+ /**
12
+ * Auth route for HTML pages - triggers XSUAA login.
13
+ * Only this route is needed; /resources and /test-resources are handled
14
+ * directly by ui5-proxy-middleware without going through approuter.
15
+ */
16
+ const UI5_SERVER_AUTH_ROUTE = {
17
+ source: String.raw `^/(test|local)/.*\.html.*$`,
18
+ destination: constants_1.UI5_SERVER_DESTINATION,
19
+ authenticationType: 'xsuaa'
20
+ };
21
+ /**
22
+ * Load xs-app.json and prepare it for the approuter (filter routes, set auth, optionally append auth route).
23
+ * Mutates and returns the config; does not build RouteEntry[].
24
+ *
25
+ * @param options - rootPath, xsappJsonPath, effectiveOptions, sourcePath.
26
+ * @returns The loaded and mutated XsappConfig.
27
+ */
28
+ function loadAndPrepareXsappConfig(options) {
29
+ const { rootPath, xsappJsonPath, effectiveOptions, sourcePath, logger } = options;
30
+ const xsappConfig = JSON.parse(node_fs_1.default.readFileSync(xsappJsonPath, 'utf8'));
31
+ const xsappRoutes = xsappConfig.routes ?? [];
32
+ xsappConfig.routes = xsappRoutes.filter((route) => {
33
+ if (route.service === 'html5-apps-repo-rt') {
34
+ logger.debug(`Filtering out xs-app.json route: service "html5-apps-repo-rt" (source: ${route.source})`);
35
+ return false;
36
+ }
37
+ const hasResources = route.source.includes('/resources') || route.source.includes('/test-resources');
38
+ if (!route.localDir && route.authenticationType === 'none' && hasResources) {
39
+ logger.debug(`Filtering out xs-app.json route: unauthenticated resource route without localDir (source: ${route.source})`);
40
+ return false;
41
+ }
42
+ return true;
43
+ });
44
+ if (effectiveOptions.disableWelcomeFile) {
45
+ delete xsappConfig.welcomeFile;
46
+ }
47
+ xsappConfig.authenticationMethod = effectiveOptions.authenticationMethod;
48
+ if (effectiveOptions.appendAuthRoute &&
49
+ effectiveOptions.authenticationMethod &&
50
+ effectiveOptions.authenticationMethod !== 'none') {
51
+ const relativeSourcePath = node_path_1.default.relative(rootPath, sourcePath);
52
+ xsappConfig.routes.push({
53
+ source: String.raw `^/([^.]+\\.html?(?:\?.*)?)$`,
54
+ localDir: relativeSourcePath,
55
+ target: '$1',
56
+ cacheControl: 'no-store',
57
+ authenticationType: 'xsuaa'
58
+ });
59
+ }
60
+ if (xsappConfig.authenticationMethod?.toLowerCase() === 'none') {
61
+ for (const route of xsappConfig.routes) {
62
+ route.authenticationType = 'none';
63
+ }
64
+ }
65
+ if (!effectiveOptions.disableUi5ServerRoutes) {
66
+ // Inject only the HTML auth route - /resources and /test-resources
67
+ // are handled directly by ui5-proxy-middleware without approuter loop
68
+ xsappConfig.routes.push(UI5_SERVER_AUTH_ROUTE);
69
+ }
70
+ return xsappConfig;
71
+ }
72
+ /**
73
+ * Build the list of route entries (compiled regex + resolved destination URLs) from a prepared xsappConfig.
74
+ * Does not read files or mutate xsappConfig.
75
+ *
76
+ * @param options - xsappConfig, effectiveOptions, logger.
77
+ * @returns Route entries for the proxy.
78
+ */
79
+ function buildRouteEntries(options) {
80
+ const { xsappConfig, effectiveOptions, logger } = options;
81
+ const routes = [];
82
+ const destList = Array.isArray(effectiveOptions.destinations) ? effectiveOptions.destinations : [];
83
+ for (const route of xsappConfig.routes ?? []) {
84
+ const routeMatch = /[^/]*\/(.*\/)?[^/]*/.exec(route.source);
85
+ if (!routeMatch) {
86
+ logger.warn(`Skipping route with source "${route.source}": could not extract path prefix.`);
87
+ continue;
88
+ }
89
+ const url = destList.find((d) => d.name === route.destination)?.url;
90
+ routes.push({
91
+ ...route,
92
+ sourcePattern: new RegExp(route.source),
93
+ path: routeMatch[1],
94
+ url
95
+ });
96
+ if (effectiveOptions.debug) {
97
+ const destination = route.destination ?? route.endpoint ?? '';
98
+ logger.debug(`Adding destination "${destination}" proxying to ${route.source}`);
99
+ }
100
+ }
101
+ return routes;
102
+ }
103
+ //# sourceMappingURL=routes.js.map