From 748c9a5a9164f51ecde2b1a24bbaaba6cefb195a Mon Sep 17 00:00:00 2001 From: programmer-singh Date: Sat, 29 Aug 2026 14:28:24 +0530 Subject: [PATCH 1/3] fix(deploy): pin pnpm below 11 and keep NODE_ENV out of the build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The App Hosting build died before installing a single dependency: Installing pnpm v12.0.0 pnpm v12.0.0 detected (>= 11.0.0), downloading tarball. Error: Cannot find module '/layers/google.nodejs.pnpm/pnpm_engine/bin/dist/pnpm.mjs' Nothing asked for pnpm 12. `packageManager` says `pnpm@10.14.0` and CI resolves that correctly, but the buildpack does not read `packageManager` — it reads `engines.pnpm`, treats it as a semver *range*, and installs the highest match from the registry. Our `">=10"` therefore meant "whatever pnpm shipped most recently", which is 12.0.0 as of today; confirmed by resolving the range against registry.npmjs.org rather than inferring it from the log. pnpm 12 then hits a broken branch in the buildpack itself: for >= 11 it unpacks the standalone GitHub release, which is a self-contained binary, but still launches it as `node /bin/dist/pnpm.mjs` — a path that exists only in the npm package layout. So the failure was never about our dependencies, and no local or CI run could have caught it, because both honour `packageManager`. `engines.pnpm` is now an exact `10.14.0`, matching `packageManager` and the lockfile it generated. This has to stay below 11 until that buildpack branch is fixed. Also drops BUILD availability from NODE_ENV, which would have been the next failure. The buildpack installs with `pnpm install --prod`, and under NODE_ENV=production that omits every devDependency — while this build is almost entirely devDependencies: the Angular CLI, the Nest CLI, typescript. Nothing in the build reads NODE_ENV (Angular takes `production` from angular.json); the only reader in the repo is `EnvService.partnerOrigin`, per request, at runtime. So RUNTIME availability loses nothing. The `Failed to find version for package @angular/core in pnpm lockfile` warnings above the failure are unrelated and expected: that is the `firebaseangular` buildpack's detect step, which looks for Angular in the root importer and finds it under `frontend:` instead. It exits 1 meaning "does not apply", which is correct — this repo builds through the generic Node path with an explicit buildCommand and runCommand. Both gotchas recorded in apphosting.yaml and CLAUDE.md, since neither is reproducible outside a real deploy. Verified locally: `pnpm install --frozen-lockfile` clean on 10.14.0, `pnpm run build` exit 0 through prerender and the SEO stamp. --- CLAUDE.md | 17 +++++++++++++++++ apphosting.yaml | 20 +++++++++++++++++++- package.json | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1d4d955..2b8c94d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -264,6 +264,23 @@ composes them in one process. - `createNodeRequestHandler` returns its argument unchanged, so the exported `reqHandler` *is* the Express app and mounts directly as middleware. +**The App Hosting buildpack reads `engines.pnpm`, and ignores `packageManager`.** +It resolves that field as a *range* against the npm registry and installs the +highest match, so `">=10"` quietly became pnpm 12 on the build machine while +local and CI stayed on 10.14.0. The buildpack's own >= 11 branch is broken — it +unpacks the standalone GitHub tarball, then launches it as +`node /bin/dist/pnpm.mjs`, which only exists in the npm package layout — +so the build died with `MODULE_NOT_FOUND` before installing a single dependency. +`engines.pnpm` is now an exact `10.14.0` matching `packageManager`. Bumping it +past 11 breaks the deploy and nothing else, so no local check will catch it. + +**Never give `NODE_ENV` BUILD availability in `apphosting.yaml`.** The buildpack +installs with `pnpm install --prod`, which under `NODE_ENV=production` drops +every devDependency — and this build *is* devDependencies: the Angular CLI, the +Nest CLI, typescript. Nothing in the build reads `NODE_ENV` anyway (Angular +takes `production` from `angular.json`); the single reader in the repo is +`EnvService.partnerOrigin`, per request, at runtime. + **`NG_ALLOWED_HOSTS` is load-bearing, and it fails silently.** Angular 21 checks the `Host` header against an allowlist (SSRF protection). Off the list it does not error — it falls back to **client-side rendering**, quietly discarding the SSR and diff --git a/apphosting.yaml b/apphosting.yaml index 41d138f..c832946 100644 --- a/apphosting.yaml +++ b/apphosting.yaml @@ -8,6 +8,16 @@ # subdirectory; `rootDir: "/"` in firebase.json keeps pnpm-workspace.yaml and # pnpm-lock.yaml at the root of the build workspace, where the installer looks. +# The pnpm version is pinned EXACTLY in package.json `engines.pnpm`, not as a +# range. The App Hosting Node buildpack resolves `engines.pnpm` against the npm +# registry and takes the highest match — it ignores `packageManager` — so the +# old `">=10"` silently selected pnpm 12. Its own >= 11 branch is broken: it +# downloads the standalone GitHub tarball and then launches it as +# `node /bin/dist/pnpm.mjs`, a path that only exists in the npm package +# layout, and the build dies with MODULE_NOT_FOUND before installing anything. +# Keep `engines.pnpm` an exact 10.x that matches `packageManager`; bumping it +# past 11 re-breaks the deploy while local and CI stay green. + runConfig: cpu: 1 memoryMiB: 1024 @@ -29,9 +39,17 @@ env: # Marks this as the deployed environment. EnvService reads it to decide that # PARTNER_DEMO_ORIGIN has no localhost default here — otherwise /agent would # embed an iframe pointing at each visitor's own machine. + # + # RUNTIME only, and that is load-bearing. `EnvService.partnerOrigin` is the + # only reader in the repo and it runs per request; nothing in the build looks + # at NODE_ENV (Angular takes `production` from angular.json, not the env). But + # the buildpack installs with `pnpm install --prod` under NODE_ENV=production, + # which drops every devDependency — and the whole build is devDependencies: + # the Angular CLI, the Nest CLI, typescript. Granting this BUILD availability + # produces a build that cannot find `ng`. - variable: NODE_ENV value: production - availability: [BUILD, RUNTIME] + availability: [RUNTIME] # Angular 21 refuses to server-render a request whose Host header is not on an # allowlist (SSRF protection). Off the allowlist it does NOT fail loudly — it diff --git a/package.json b/package.json index bd18a89..0279423 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "AI-native expense intelligence platform with a universal WebMCP Copilot", "engines": { "node": ">=22", - "pnpm": ">=10" + "pnpm": "10.14.0" }, "scripts": { "dev": "pnpm run build:shared && concurrently -n backend,frontend,partner -c blue,magenta,yellow \"pnpm run dev:backend\" \"pnpm run dev:frontend\" \"pnpm run dev:partner\"", From 2d1a9ff8391bfdd4ca283cf30f4435679c4b238e Mon Sep 17 00:00:00 2001 From: programmer-singh Date: Sat, 29 Aug 2026 14:32:02 +0530 Subject: [PATCH 2/3] fix(deploy): restore *.hosted.app to the SSR allowlist, and give PUBLIC_ORIGIN a scheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems that would have survived a green build, found while confirming the pnpm fix against the real backend. **`*.hosted.app` was missing from NG_ALLOWED_HOSTS, and it is the domain this service actually answers on.** The backend serves `actuo--actuo-2f1f3.asia-east1.hosted.app`; the allowlist listed `*.web.app` and `*.firebaseapp.com`, which are Firebase Hosting domains, not App Hosting ones. I introduced this in the previous commit and described `*.hosted.app` as "not a domain this deploy serves from", which was wrong — `apphosting:backends:list` says otherwise. Off the allowlist Angular does not error, it silently renders client-side, so the symptom would have been a working site with the SSR and structured-data work in §8.5 quietly gone. Checked the matching rule rather than assuming it: `isHostAllowed` turns `*.x` into `hostname.endsWith('.x')`, so one wildcard covers the multi-label `actuo--actuo-2f1f3.asia-east1.hosted.app`. The other entries are kept — they cost nothing and cover a Firebase Hosting rewrite if one is ever put in front. **PUBLIC_ORIGIN had no scheme.** stamp-seo.mjs substitutes the value verbatim, so `actuo.programmersingh.dev` produced `actuo.programmersingh.dev/` — which `new URL()` rejects. The build would not have failed; the sitemap would just have been invalid and og:image unresolvable. Now `https://actuo.programmersingh.dev`, and the requirement is written next to the value because the variable name does not imply it. That domain is NXDOMAIN today, which is a deliberate choice to keep the canonical pointing where the site will live rather than where it currently is. Recorded as a prerequisite in the file: attach it to the backend and point DNS, or the canonical advertises a dead host. Verified: build exit 0, 28 URLs stamped absolute across 7 files, sitemap `` and `` both valid absolute URLs, and `ng-server-context` still present in the prerendered index.html. --- apphosting.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apphosting.yaml b/apphosting.yaml index c832946..7bea59c 100644 --- a/apphosting.yaml +++ b/apphosting.yaml @@ -67,7 +67,7 @@ env: # Verify after a deploy: the HTML for `/` must contain `ng-server-context`. # If it does not, this value is wrong and the site is rendering client-side. - variable: NG_ALLOWED_HOSTS - value: "*.web.app,*.run.app,*.firebaseapp.com,*.programmersingh.dev" + value: "*.hosted.app,*.run.app,*.web.app,*.firebaseapp.com,*.programmersingh.dev" availability: [RUNTIME] # The Supabase project URL is not a credential; the service-role key below is. @@ -101,10 +101,21 @@ env: # from and the value has to be present while the build runs. # # Unset, every URL stays root-relative — valid, but the sitemap is not - # spec-compliant and scrapers may not resolve og:image. Set it to the - # backend's real hostname. + # spec-compliant and scrapers may not resolve og:image. + # + # This is an ORIGIN, so it must carry the scheme. stamp-seo.mjs substitutes the + # value verbatim (it only trims trailing slashes), so a bare hostname yields + # `actuo.programmersingh.dev/` — not a URL, and `new URL()` rejects + # it. Nothing fails the build; the sitemap is just quietly invalid. + # + # PREREQUISITE: this domain must actually resolve and be attached to the + # backend. It was NXDOMAIN when this value was set, and a canonical pointing at + # a host that does not resolve is worse than a relative one — it tells crawlers + # the real URL is dead. Add it as a custom domain on the App Hosting backend, + # point DNS at it, then confirm with `host actuo.programmersingh.dev`. Until + # that is done the deploy is fine and only the SEO metadata is wrong. - variable: PUBLIC_ORIGIN - value: "actuo.programmersingh.dev" + value: "https://actuo.programmersingh.dev" availability: [BUILD] # --- Optional, with sane defaults in EnvService -------------------------- From 628c13157e437209bdb560b4d3023e0083b9f4a7 Mon Sep 17 00:00:00 2001 From: programmer-singh Date: Sun, 30 Aug 2026 03:59:03 +0530 Subject: [PATCH 3/3] fix(dependencies): revert pnpm version to <10.0.0 and update packageManager to 9.15.5 --- package.json | 4 ++-- pnpm-lock.yaml | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 0279423..fd17976 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "AI-native expense intelligence platform with a universal WebMCP Copilot", "engines": { "node": ">=22", - "pnpm": "10.14.0" + "pnpm": ">=9.0.0 <10.0.0" }, "scripts": { "dev": "pnpm run build:shared && concurrently -n backend,frontend,partner -c blue,magenta,yellow \"pnpm run dev:backend\" \"pnpm run dev:frontend\" \"pnpm run dev:partner\"", @@ -23,5 +23,5 @@ "concurrently": "^10.0.5", "supabase": "^2.116.0" }, - "packageManager": "pnpm@10.14.0" + "packageManager": "pnpm@9.15.5+sha512.845196026aab1cc3f098a0474b64dfbab2afe7a1b4e91dd86895d8e4aa32a7a6d03049e2d0ad770bbe4de023a7122fb68c1a1d6e0d033c7076085f9d5d4800d4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 483008d..053e049 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,7 +150,7 @@ importers: devDependencies: '@angular/build': specifier: ^21.2.21 - version: 21.2.22(addca2e8b0d1cdbe785c1221ede9343a) + version: 21.2.22(tvsdneph7b2ppo44lsnrlossqi) '@angular/cli': specifier: ^21.2.21 version: 21.2.22(@types/node@20.19.43)(chokidar@5.0.0) @@ -2782,8 +2782,8 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express-rate-limit@8.6.2: - resolution: {integrity: sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==} + express-rate-limit@8.7.0: + resolution: {integrity: sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -2982,8 +2982,8 @@ packages: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} - ip-address@10.5.0: - resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} + ip-address@10.7.0: + resolution: {integrity: sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -4441,7 +4441,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular/build@21.2.22(addca2e8b0d1cdbe785c1221ede9343a)': + '@angular/build@21.2.22(tvsdneph7b2ppo44lsnrlossqi)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.22(chokidar@5.0.0) @@ -5208,7 +5208,7 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.1.1 express: 5.2.1 - express-rate-limit: 8.6.2(express@5.2.1) + express-rate-limit: 8.7.0(express@5.2.1) hono: 4.13.5 jose: 6.2.10 json-schema-typed: 8.0.2 @@ -6088,7 +6088,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.97.3)) + vitest: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.97.3)) '@vitest/expect@4.1.11': dependencies: @@ -6667,11 +6667,11 @@ snapshots: exponential-backoff@3.1.3: {} - express-rate-limit@8.6.2(express@5.2.1): + express-rate-limit@8.7.0(express@5.2.1): dependencies: debug: 4.4.3 express: 5.2.1 - ip-address: 10.5.0 + ip-address: 10.7.0 transitivePeerDependencies: - supports-color @@ -6924,7 +6924,7 @@ snapshots: through: 2.3.8 wrap-ansi: 6.2.0 - ip-address@10.5.0: {} + ip-address@10.7.0: {} ipaddr.js@1.9.1: {} @@ -7828,7 +7828,7 @@ snapshots: socks@2.8.9: dependencies: - ip-address: 10.5.0 + ip-address: 10.7.0 smart-buffer: 4.2.0 source-map-js@1.2.1: {}