# Endpoint configuration rules for the B2G frontends

**Audience:** frontend developers working on `b2g`, `gMarket`, `b2g_website` and `b2g_filems_fe`.
`b2g_filems_fe` is the micro-frontend the other apps embed — see §4.

**Why this exists:** a page served from the UAT load balancer was making API calls to the *production*
site. The browser blocked them, and the error looked like a CORS misconfiguration. It was not — it
was a hardcoded hostname. This document explains the rule, the audit results, and what each team
needs to change.

---

## 1. The incident, decoded

Page opened: `http://b2g-uat-alb-105245781.ap-south-1.elb.amazonaws.com/app/usermanagement/adddivision`

Request the browser blocked:
```
https://demo.blu2green.earth/filemst/api/drv/drive/filemst?uac=f9d6c6ad2e0f8bfded8c4c37e4140629
```

```
Access to XMLHttpRequest at 'https://demo.blu2green.earth/...' from origin
'http://b2g-uat-alb-105245781.ap-south-1.elb.amazonaws.com' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values
'*, http://b2g-uat-alb-105245781.ap-south-1.elb.amazonaws.com', but only one is allowed.
```

There are **two independent defects** in that one message, and it matters not to conflate them.

### Defect 1 — the request should never have left the environment

`b2g/src/assets/env.js` was committed containing:

```js
window.__env.microfeHost = "https://demo.blu2green.earth/menu/";
```

`b2g/src/index.html` uses that value to inject a remote micro-frontend:

```html
<script src="assets/env.js"></script>
<script>
  document.write('<script src="' + window.__env.microfeHost + '/micro-fe.js" type="module"><\/script>');
  document.write('<link rel="stylesheet" href="' + window.__env.microfeHost + '/styles.css">');
</script>
```

So the UAT deployment loaded its menu **from the production site**, and that menu code then called
production's API. The same file in gMarket pointed at `http://192.168.1.58:8080`, an office LAN
address no browser outside the office can reach.

Critically, `assets/env.js` is **not** an Angular `environment.*.ts` file. It is a plain script loaded
at runtime, so it is invisible to `fileReplacements` and to every build configuration. Work done to
make the bundles host-agnostic did not cover it.

### Defect 2 — the duplicated CORS header is on the production server

`Access-Control-Allow-Origin: *, http://b2g-uat-...` is two values in one header. A browser rejects
that outright, which is why the failure was hard rather than a silent empty response. It happens when
**two layers both add the header** — typically a web-server `add_header` plus the application's own
CORS filter.

Measured on the UAT environment for comparison, with both a foreign origin and one that is in the
Yii `corsOrigins` allowlist:

```
$ curl -s -D - -H "Origin: http://192.168.1.152:1111" http://<alb>/micro/web/master/master/getcountrylist
access-control-allow-origin: *          <- exactly one header, every time
```

UAT is clean because Kong's CORS plugin *overwrites* the header rather than appending. **The duplicate
comes from `demo.blu2green.earth`**, where a server-level `Access-Control-Allow-Origin *` sits on top
of the Yii `Cors` filter that echoes the request origin. Whoever owns that host should remove one of
the two — keep the application filter and drop the web-server header, since only the application
knows the allowlist.

Fixing defect 2 alone would be the wrong outcome: it would make UAT *successfully* read and write
production data.

---

## 2. The rule

> **A deployed frontend must never contain the hostname of the environment it is deployed to, nor of
> any other environment.**

Three mechanisms, in order of preference:

| Use | When | Where it lives |
|---|---|---|
| **Relative path** — `/api`, `/micro/web` | The service is behind the same gateway as the SPA. This is the normal case. | `environment.*.ts` |
| **Runtime config** — `window.__env.*` | The value is not known at build time, or one image must serve several tiers. | `assets/env.js`, generated at container start |
| **Compile-time constant** | Genuinely external third party — PostHog, reCAPTCHA, a government site. | `environment.*.ts` |

Everything in the B2G platform is served through one Kong gateway behind one load balancer, so
**every platform call is same-origin and belongs in category one**. That is also why no CORS
preflight occurs in normal operation — and why a CORS error is a reliable signal that something has
escaped its environment.

### How runtime config now works

`assets/env.js` is **generated when the container starts** by
[`docker/spa-entrypoint.sh`](../docker/spa-entrypoint.sh), from the `MICROFE_HOST` environment
variable set on the ECS task definition:

```sh
cat > "$ENV_JS" <<EOF
(function (window) {
  window.__env = window.__env || {};
  window.__env.microfeHost = "$(json_escape "$MICROFE_HOST")";
})(this);
EOF
```

Consequences you need to know:

- **The committed `src/assets/env.js` is for `ng serve` only.** It is overwritten in every container.
  Its default is now empty.
- **Changing the value needs no rebuild.** It is a task-definition environment variable:
  ```powershell
  # inspect
  aws ecs describe-task-definition --task-definition b2g-fe-b2g --region ap-south-1 `
    --query "taskDefinition.containerDefinitions[0].environment"
  ```
- **Empty means "do not inject".** `index.html` now checks the value and skips injection rather than
  requesting `/micro-fe.js` from its own origin and 404-ing on every page load.

`index.html` after the change:

```html
<script src="assets/env.js"></script>
<script>
  (function () {
    var host = (window.__env && window.__env.microfeHost) || '';
    if (!host) { return; }                 // no remote micro-frontend in this environment
    host = host.replace(/\/+$/, '');       // tolerate a trailing slash in the config
    document.write('<script src="' + host + '/micro-fe.js" type="module"><\/script>');
    document.write('<link rel="stylesheet" href="' + host + '/styles.css">');
  })();
</script>
```

---

## 3. Audit results

Scanned all `.ts`, `.html` and `.js` under the four apps, excluding `node_modules`, `.angular` and
third-party CDNs: **426 absolute URLs**. Most are legitimate — news articles, government sites,
social links, geo/weather APIs.

Filtering to *platform* hosts (`blu2green.earth`, `businessgateways`, `192.168.x`, `172.16.x`,
`localhost`) **outside** `environment.*.ts`, which is the only correct home for a per-tier host:

### Fixed in this change

| File | Was | Now |
|---|---|---|
| `b2g/src/assets/env.js` | `https://demo.blu2green.earth/menu/` | generated at start-up; committed default empty |
| `gMarket/src/assets/env.js` | `http://192.168.1.58:8080` | same |
| `b2g/assets/env.js` | `http://192.168.1.152:8080` | **deleted** — outside `src/`, so `angular.json`'s `src/assets` glob never included it in any build. Dead file with a stale host. |
| `b2g/src/index.html` | injection block present **twice** — the micro-frontend and its stylesheet were loaded and applied twice per page | consolidated to one guarded block |
| `gMarket/src/index.html` | unguarded injection | guarded |
| `b2g/src/app/gmarket/gmarket-landing-page/gmarket-landing-page.ts:215` | `redirectUrl: 'http://localhost:4300'` — a user-facing "Sell on gMarket" redirect that sent every deployed user to a developer's port | `environment.gmarketAppUrl` |
| `b2g_filems_fe/package.json` | no docker/ECS bundle script existed, so `micro-fe.js` was never built here | new `bundle-docker` script |
| `b2g_filems_fe/Dockerfile` | `ng build --configuration=docker` produced hashed bundles and **no `micro-fe.js`** | `npm run bundle-docker` |
| task defs `b2g-fe-b2g`, `b2g-fe-gmarket` | menu loaded from `demo.blu2green.earth` | `MICROFE_HOST=/filems` — our own origin |

### Traps left in place — please review

Not changed because each is currently inert and changing it blind risks altering behaviour. Each
would misfire the moment a surrounding condition changes.

| File | Issue |
|---|---|
| `b2g/src/app/digitall/services/esg-subscription.service.ts:79` | `environment.digtall \|\| 'http://172.16.16.153:86/b2g/b2g_digitall/web'`. The fallback never fires today because `digtall` is set, but if it were ever empty the app would silently call an office LAN host instead of failing loudly. Prefer throwing on missing config. |
| `b2g/src/app/digitall/division-survey-apply/division-survey-apply.ts:189,229` | `http://localhost:4200` is in a `postMessage` **allowed-origins** list alongside the Synesgy hosts. Deliberate for local testing, but it means a deployed build trusts messages from a local origin. Gate it on a non-production flag. |
| `b2g_filems_fe/src/environments/environment.docker.ts` | Six of nine base URLs are LAN hosts — `192.168.1.140:5500`, `192.168.1.58:83`, `192.168.1.58:86`, `192.168.1.140:4200`, `192.168.1.140:86`, `192.168.1.58:4400`. Only `b2gapiBaserUrl` reaches this platform. The File Manager will show partial data in any environment outside the office until these are made configurable. |
| `b2g_filems_fe/src/app/@shared/drive.service.ts:85` and `remote.service.ts:195` | `uac=f9d6c6ad2e0f8bfded8c4c37e4140629` is a hardcoded access token in client-side source. It is in the repository and shipped to every browser. Rotate it and move it server-side. |
| `b2g/src/app/layouts/auth-shell/auth-shell.component.html` | Five `https://demo.blu2green.earth/` marketing links. Low risk since they are `target="_blank"` navigations, but they hard-link UAT to the demo site. |

---

## 4. The micro-frontend IS in this repository — `b2g_filems_fe`

An earlier draft of this document said the micro-frontend was a separate codebase. That was
wrong, and the correction matters because it means the whole thing is fixable here.

`b2g_filems_fe` **is** the micro-frontend:

```json
// b2g_filems_fe/package.json
"name": "micro-fe",
// angular.json project name: microFe
```

`micro-fe.js` is not produced by `ng build`. It is produced by a separate npm script that
concatenates the unhashed bundles:

```json
"bundle":       "ng build --configuration production --output-hashing=none --delete-output-path=false && node concat.js",
"bundle-demo":  "ng build --configuration demo --output-hashing=none --delete-output-path=false --base-href=https://blu2green.earth/menu/ && node concat.js",
```

```js
// concat.js
await concat(["./app/runtime.js", "./app/polyfills.js", "./app/main.js"], "./app/micro-fe.js");
```

### Why the host apps were loading it from production

There was **no docker/ECS equivalent of those scripts**. `b2g_filems_fe/Dockerfile` ran a plain
`ng build --configuration=docker`, and the docker configuration sets `outputHashing: "all"` — so the
image contained `main.<hash>.js` and **no `micro-fe.js` at all**. `/filems/micro-fe.js` returned 404.

The only place a `micro-fe.js` existed was `https://demo.blu2green.earth/menu/`, built by
`bundle-demo` — which bakes in `environment.demo.ts`:

```ts
b2gapiBaserUrl: 'https://blu2green.earth/filemst/api/'
```

So the host app loaded the menu from production, and the menu called production's filemst, because
that absolute host is compiled into that particular bundle. Nothing on the deployment side could
redirect it.

### The fix

| Change | Effect |
|---|---|
| `package.json` — new `bundle-docker` script | `ng build --configuration docker --output-hashing=none --delete-output-path=false && node concat.js`. Hashing must be off; `concat.js` reads the unhashed filenames. `baseHref` `/filems/` comes from `angular.json`'s docker configuration. |
| `b2g_filems_fe/Dockerfile` — `RUN npm run bundle-docker` | the image now actually contains `app/micro-fe.js` and `app/styles.css` |
| `environment.docker.ts` — `const KONG = ''` | `b2gapiBaserUrl` becomes `/filemst/api/`, a **relative** path, so the micro-frontend calls whatever origin served it |
| Task definitions `b2g-fe-b2g:4`, `b2g-fe-gmarket:4` — `MICROFE_HOST=/filems` | the host apps load `/filems/micro-fe.js` from **our own origin** |

End result for the reported request:

```
before  https://demo.blu2green.earth/filemst/api/drv/drive/filemst?uac=...   cross-origin -> CORS block
after   /filemst/api/drv/drive/filemst?uac=...                              same-origin  -> our filemst
```

Same-origin means **no preflight is issued at all**, so the duplicated
`Access-Control-Allow-Origin` on the production host stops mattering for this path. That header is
still worth fixing on `demo.blu2green.earth` (see §1, defect 2) for anything else that calls it
cross-origin, but it is no longer on the critical path here.

`MICROFE_HOST` is a task-definition variable, so switching environments needs no rebuild. Setting it
empty disables the injection entirely, which is the right value for any tier that has no
micro-frontend deployed.

### Still to do in `b2g_filems_fe`

`environment.demo.ts` and `environment.prod.ts` still carry absolute hosts
(`https://blu2green.earth/filemst/api/` and `http://192.168.1.58:86/b2gnew/b2g_filemst/api/`). Since
every tier serves the SPA and the API through one gateway, these can and should become relative too —
then no build configuration can escape its own origin. The `prod` one also uses a different path
layout (`/b2gnew/...` rather than `/filemst/api/`), so confirm which is current before changing it.

## 5. Checklist for a pull request

- [ ] No new absolute URL to a platform host anywhere outside `environment.*.ts`.
- [ ] New API base URLs are **relative** — `/usermg/web`, not `https://host/usermg/web`.
- [ ] Anything environment-specific and not known at build time goes through `window.__env`, and
      `docker/spa-entrypoint.sh` plus the task definition are updated together.
- [ ] No `|| 'http://<host>'` fallbacks. If required config is missing, fail loudly.
- [ ] No credentials, tokens or `uac=` values in client-side source.
- [ ] Grep before pushing:
      ```bash
      grep -rnE "https?://(192\.168|172\.16|10\.|localhost|.*blu2green\.earth)" \
        --include=*.ts --include=*.html --include=*.js <app>/src \
        | grep -v "src/environments/environment\."
      ```
      Anything this prints needs a justification in the PR description.
