@zerotal/core 1.15.1 → 1.15.3

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/CHANGELOG.md CHANGED
@@ -8,6 +8,28 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.15.2] — 2026-10-23
12
+
13
+ ### Fixed
14
+
15
+ - **A production build ships React's production build.** `browserEnvDefines()` defined
16
+ `import.meta.env` but not `process.env.NODE_ENV`, which is the switch React and most of
17
+ npm read — and Bun.build leaves that expression to whatever the shell has. An app that ran
18
+ `inertia:build --production` without `NODE_ENV` in its environment shipped React's
19
+ development build: a 522 KB vendor chunk where 322 KB would do, the DevTools notice in
20
+ every visitor's console, and full hydration diffs. The define now states it from the same
21
+ flag. Found on a live consumer app (Trekly).
22
+
23
+ - **`back()` works behind a proxy that terminates TLS.** `safeRedirectPath` — which
24
+ `ctx.back()`, `validate()`'s redirect on failure, `FormRequest`, the intended-URL helpers
25
+ and flow all use — accepted a Referer only from the request's own origin. Behind Caddy or
26
+ nginx the request reaches the process as `http://…` while every browser sends
27
+ `https://…`, so every Referer was refused and "back" was always `/`: a form with one
28
+ mistake in it sent the user to the home page, its errors flashed to a page that never
29
+ shows them. The configured `app.url`'s origin now counts as the app's own as well; every
30
+ other site is still refused. Found on a live consumer app (Trekly), which had worked
31
+ round it in its exception handler.
32
+
11
33
  ## [1.15.1] — 2026-09-13
12
34
 
13
35
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/core",
3
- "version": "1.15.1",
3
+ "version": "1.15.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -37,5 +37,13 @@ export function browserEnvDefines(isProduction: boolean): Record<string, string>
37
37
  PROD: isProduction,
38
38
  MODE: isProduction ? "production" : "development",
39
39
  }),
40
+ // The other half of the convention, and the one that costs the most to miss.
41
+ // React and most of npm choose their production build with
42
+ // `process.env.NODE_ENV === "production"`, and Bun.build leaves the expression
43
+ // to the environment it runs in. A deploy that ran `inertia:build --production`
44
+ // without NODE_ENV set shipped React's development build — a 522 KB vendor
45
+ // chunk where 322 KB would do, the DevTools notice in every visitor's console,
46
+ // and full hydration diffs to anyone who looked. Stated here, the flag decides.
47
+ "process.env.NODE_ENV": JSON.stringify(isProduction ? "production" : "development"),
40
48
  };
41
49
  }
@@ -14,6 +14,7 @@ import {
14
14
  DEFAULT_MD_OPTIONS,
15
15
  type BunMarkdownOptions,
16
16
  } from "../helpers/markdown.ts";
17
+ import { config } from "../helpers/config.ts";
17
18
  import type { SessionContract, TransactionContext } from "../contracts/index.ts";
18
19
  import type { ContextRegistry, ContextKey } from "./ContextRegistry.ts";
19
20
 
@@ -868,14 +869,24 @@ const _prototypeMethods: ReadonlyArray<[string, (...args: unknown[]) => unknown]
868
869
  })();
869
870
 
870
871
  /**
871
- * Return `url` only when it belongs to the same `origin`, otherwise `undefined`.
872
+ * Return `url` only when it belongs to the app, otherwise `undefined`.
872
873
  * Guards `ctx.back()` and similar helpers against open-redirect attacks by
873
874
  * rejecting absent, malformed, or cross-origin URLs.
874
875
  *
876
+ * "Belongs to the app" is the request's own `origin` **or the configured
877
+ * `app.url`'s**. The second matters behind a reverse proxy that terminates TLS:
878
+ * the request reaches the process as `http://…`, while every browser sends its
879
+ * `Referer` as `https://…`, so a same-origin check alone refused every one of
880
+ * them and `back()` — and `validate()`'s redirect on failure — fell back to "/"
881
+ * on every request in production. A form with one mistake in it sent the user
882
+ * to the home page, its errors flashed to a page that never shows them. The
883
+ * configured URL is the app's own address by definition, so accepting it opens
884
+ * nothing to anyone else.
885
+ *
875
886
  * @param url - Candidate redirect target (e.g. a `Referer` header value).
876
887
  * @param origin - The current request origin to match against.
877
- * @returns The same-origin `url`, or `undefined` when it is absent, unparseable,
878
- * or points to a different origin.
888
+ * @returns The `url`, or `undefined` when it is absent, unparseable, or points
889
+ * anywhere but this app.
879
890
  *
880
891
  * @internal
881
892
  */
@@ -885,7 +896,20 @@ export function safeRedirectPath(
885
896
  ): string | undefined {
886
897
  if (!url) return undefined;
887
898
  try {
888
- return new URL(url).origin === origin ? url : undefined;
899
+ const target = new URL(url).origin;
900
+ if (target === origin) return url;
901
+ return target === configuredOrigin() ? url : undefined;
902
+ } catch {
903
+ return undefined;
904
+ }
905
+ }
906
+
907
+ /** The configured `app.url`'s origin, or `undefined` off-app or when it is unset or malformed. */
908
+ function configuredOrigin(): string | undefined {
909
+ const base = config.safe("app.url", "") as string;
910
+ if (!base) return undefined;
911
+ try {
912
+ return new URL(base).origin;
889
913
  } catch {
890
914
  return undefined;
891
915
  }