@multiplatform.one/core 7.0.0 → 7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/core",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "Core framework shell, provider composition, and layout factories for multiplatform.one",
5
5
  "keywords": [
6
6
  "app",
@@ -46,11 +46,11 @@
46
46
  "@tanstack/react-form-devtools": "0.2.22",
47
47
  "@tanstack/react-pacer-devtools": "^0.7.0",
48
48
  "react-cookie": "^8.1.2",
49
- "@multiplatform.one/i18n": "7.0.0",
50
- "@multiplatform.one/logger": "7.0.0",
51
- "@multiplatform.one/platform": "7.0.0",
52
- "@multiplatform.one/store": "7.0.0",
53
- "@multiplatform.one/theme": "7.0.0"
49
+ "@multiplatform.one/i18n": "7.1.0",
50
+ "@multiplatform.one/logger": "7.1.0",
51
+ "@multiplatform.one/platform": "7.1.0",
52
+ "@multiplatform.one/store": "7.1.0",
53
+ "@multiplatform.one/theme": "7.1.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@sentry/react": "^10.51.0",
@@ -60,14 +60,16 @@
60
60
  "i18next": "^25.10.10",
61
61
  "one": "^1.16.5",
62
62
  "react": "19.2.5",
63
+ "react-dom": "19.2.5",
63
64
  "react-i18next": "^16.6.6",
64
65
  "react-native": "0.83.2",
65
66
  "react-native-gesture-handler": "~2.31.1",
66
67
  "react-native-safe-area-context": "~5.7.0",
67
68
  "tamagui": "2.7.6",
68
69
  "typescript": "~5.9.3",
69
- "@multiplatform.one/frappe": "7.0.0",
70
- "@multiplatform.one/web3": "7.0.0"
70
+ "vitest": "^4.1.5",
71
+ "@multiplatform.one/frappe": "7.1.0",
72
+ "@multiplatform.one/web3": "7.1.0"
71
73
  },
72
74
  "peerDependencies": {
73
75
  "@sentry/react": "^9.0.0",
@@ -83,10 +85,10 @@
83
85
  "react-native-gesture-handler": ">=2.0.0",
84
86
  "react-native-safe-area-context": ">=4.0.0",
85
87
  "tamagui": "^2.0.0-rc",
86
- "@multiplatform.one/frappe": "^7.0.0",
87
- "@multiplatform.one/frappe-ui": "^7.0.0",
88
- "@multiplatform.one/keycloak": "^7.0.0",
89
- "@multiplatform.one/web3": "^7.0.0"
88
+ "@multiplatform.one/frappe": "^7.1.0",
89
+ "@multiplatform.one/frappe-ui": "^7.1.0",
90
+ "@multiplatform.one/keycloak": "^7.1.0",
91
+ "@multiplatform.one/web3": "^7.1.0"
90
92
  },
91
93
  "peerDependenciesMeta": {
92
94
  "@multiplatform.one/web3": {
@@ -8,6 +8,7 @@ import { config, isDev, platform } from "@multiplatform.one/platform";
8
8
  import { createI18nConfig } from "@multiplatform.one/i18n";
9
9
  import { logger } from "@multiplatform.one/logger";
10
10
  import { storage } from "@multiplatform.one/store";
11
+ import { RuntimePublicConfigScript } from "./RuntimePublicConfigScript";
11
12
  import type { CreateRootLayoutConfig, RootLoaderData } from "./rootLayoutShared";
12
13
 
13
14
  export type {
@@ -142,6 +143,14 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
142
143
  <html lang={htmlLang}>
143
144
  <head>
144
145
  <meta charSet="utf-8" />
146
+ {/* RUNTIME public config, first thing after charset so it is parsed
147
+ and executed before the deferred app bundle constructs Config.
148
+ Without it the browser has NO path to config at all: Config's env
149
+ lookup reads process.env, which does not exist there, so a
150
+ container env var (MARKETPLACE_API_URL, FRAPPE_ENABLED, …) could
151
+ never reach the SPA and the image had to be rebuilt per
152
+ environment. See RuntimePublicConfigScript. */}
153
+ <RuntimePublicConfigScript />
145
154
  {interFonts && (
146
155
  <>
147
156
  <link
@@ -0,0 +1,73 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { runtimePublicConfigKey } from "@multiplatform.one/platform";
5
+ import { renderToStaticMarkup } from "react-dom/server";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { RuntimePublicConfigScript } from "./RuntimePublicConfigScript";
8
+
9
+ // vitest runs this suite in the node environment, so `window` is undefined and
10
+ // `platform.isServer` is true — i.e. every render below is the SSR branch, the
11
+ // one that actually writes the served document.
12
+ function renderDocument(): string {
13
+ return renderToStaticMarkup(<RuntimePublicConfigScript />);
14
+ }
15
+
16
+ afterEach(() => {
17
+ vi.unstubAllEnvs();
18
+ });
19
+
20
+ describe("RuntimePublicConfigScript", () => {
21
+ it("emits the public payload into the SSR document", () => {
22
+ vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["MARKETPLACE_API_URL"]));
23
+ vi.stubEnv("MARKETPLACE_API_URL", "https://api.marketplace.example.org");
24
+ const html = renderDocument();
25
+ // The whole point of the seam: the VALUE the SSR process holds in its own
26
+ // env at request time is in the bytes the browser receives. Nothing about
27
+ // this depends on what the image was BUILT with.
28
+ expect(html).toContain("<script");
29
+ expect(html).toContain(runtimePublicConfigKey);
30
+ expect(html).toContain("https://api.marketplace.example.org");
31
+ });
32
+
33
+ it("emits FRAPPE_ENABLED so the frappe provider can mount in the browser", () => {
34
+ // CreateApp.tsx: `cfg.enabled ?? config.get("FRAPPE_ENABLED") === "1"`.
35
+ vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["FRAPPE_ENABLED"]));
36
+ vi.stubEnv("FRAPPE_ENABLED", "1");
37
+ expect(renderDocument()).toContain('"FRAPPE_ENABLED":"1"');
38
+ });
39
+
40
+ it("keeps a private key out of the SSR document", () => {
41
+ vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["MARKETPLACE_API_URL"]));
42
+ vi.stubEnv("MARKETPLACE_API_URL", "https://api.marketplace.example.org");
43
+ vi.stubEnv("SECRET", "hunter2");
44
+ vi.stubEnv("DATABASE_PASSWORD", "hunter2");
45
+ const html = renderDocument();
46
+ expect(html).not.toContain("SECRET");
47
+ expect(html).not.toContain("DATABASE_PASSWORD");
48
+ expect(html).not.toContain("hunter2");
49
+ });
50
+
51
+ it("cannot be closed out of by a value that looks like markup", () => {
52
+ vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["BASE_URL"]));
53
+ vi.stubEnv("BASE_URL", "</script><script>alert(1)</script>");
54
+ const html = renderDocument();
55
+ expect(html).not.toContain("</script><script>");
56
+ expect(html).toContain("\\u003c");
57
+ });
58
+ });
59
+
60
+ describe("createRootLayout (web)", () => {
61
+ it("mounts RuntimePublicConfigScript in the document head", () => {
62
+ // The component only reaches a browser if the root layout renders it, and
63
+ // the root layout is a `one` route module that cannot be instantiated
64
+ // outside a router — so the mount is pinned at the source level. Delete
65
+ // the element from <head> and this reds.
66
+ const source = readFileSync(
67
+ join(dirname(fileURLToPath(import.meta.url)), "CreateRootLayout.web.tsx"),
68
+ "utf-8",
69
+ );
70
+ const head = source.slice(source.indexOf("<head>"), source.indexOf("</head>"));
71
+ expect(head).toContain("<RuntimePublicConfigScript />");
72
+ });
73
+ });
@@ -0,0 +1,54 @@
1
+ import {
2
+ platform,
3
+ runtimePublicConfigKey,
4
+ serializeRuntimePublicConfig,
5
+ } from "@multiplatform.one/platform";
6
+
7
+ /**
8
+ * Publishes the SSR-resolved PUBLIC config into the served document.
9
+ *
10
+ * Mounted in `<head>` by `createRootLayout` (web). Deliberately the SAME shape
11
+ * one uses for `__one_server_context__` — an inline `<script>` assigning a
12
+ * `globalThis` key, `async` + `href` so React 19 keys it stably, and
13
+ * `suppressHydrationWarning` because the client renders an EMPTY body for the
14
+ * already-executed script (one's `server/ServerContextScript` does exactly
15
+ * this). It is a sibling payload on the established seam, not a second
16
+ * transport.
17
+ *
18
+ * XSS: the injected string is NOT user content. It is built by
19
+ * `serializeRuntimePublicConfig`, which (a) walks the BUILD-TIME public key
20
+ * allowlist rather than any request-derived input, and (b) emits the values
21
+ * through a JSON serializer that escapes `<`, `>`, U+2028 and U+2029 — so the
22
+ * body cannot close its own `</script>` tag or terminate a JS statement. This
23
+ * is the same sanitizer shape one ships as `utils/htmlEscape.safeJsonStringify`
24
+ * for the server-context payload. React offers no other way to emit a script
25
+ * body: text children of `<script>` are HTML-escaped, which would corrupt the
26
+ * JSON quoting.
27
+ *
28
+ * Ordering: an inline classic script in `<head>` runs during document parse,
29
+ * while the app bundle is a deferred module script — so `globalThis` already
30
+ * carries the payload by the time `@multiplatform.one/platform` constructs its
31
+ * `Config`.
32
+ *
33
+ * On the browser the body is empty: `serializeRuntimePublicConfig()` reads
34
+ * `process.env`, which only exists on the server. Rendering it client-side
35
+ * would publish an EMPTY payload over the real one.
36
+ */
37
+ export function RuntimePublicConfigScript() {
38
+ return (
39
+ <script
40
+ async
41
+ // @ts-expect-error `href` is not on React's ScriptHTMLAttributes — React 19
42
+ // keys hoisted scripts by it anyway, and `one`'s ServerContextScript (the
43
+ // sibling payload this deliberately mirrors) carries the identical
44
+ // suppression on the identical line. Copying the transport means copying
45
+ // this, not inventing a differently-keyed script.
46
+ href={runtimePublicConfigKey}
47
+ suppressHydrationWarning
48
+ dangerouslySetInnerHTML={{
49
+ __html: platform.isServer ? serializeRuntimePublicConfig() : "",
50
+ }}
51
+ />
52
+ );
53
+ }
54
+ RuntimePublicConfigScript.displayName = "RuntimePublicConfigScript";
@@ -1 +1 @@
1
- {"version":3,"file":"CreateRootLayout.web.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.web.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAOtD,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,oBAAoB,CAAC;AAEjF,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CAoL3E"}
1
+ {"version":3,"file":"CreateRootLayout.web.d.ts","sourceRoot":"","sources":["../../src/app/CreateRootLayout.web.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAQtD,OAAO,KAAK,EAAE,sBAAsB,EAAkB,MAAM,oBAAoB,CAAC;AAEjF,YAAY,EACV,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,sBAAsB,GAAG,aAAa,CA4L3E"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Publishes the SSR-resolved PUBLIC config into the served document.
3
+ *
4
+ * Mounted in `<head>` by `createRootLayout` (web). Deliberately the SAME shape
5
+ * one uses for `__one_server_context__` — an inline `<script>` assigning a
6
+ * `globalThis` key, `async` + `href` so React 19 keys it stably, and
7
+ * `suppressHydrationWarning` because the client renders an EMPTY body for the
8
+ * already-executed script (one's `server/ServerContextScript` does exactly
9
+ * this). It is a sibling payload on the established seam, not a second
10
+ * transport.
11
+ *
12
+ * XSS: the injected string is NOT user content. It is built by
13
+ * `serializeRuntimePublicConfig`, which (a) walks the BUILD-TIME public key
14
+ * allowlist rather than any request-derived input, and (b) emits the values
15
+ * through a JSON serializer that escapes `<`, `>`, U+2028 and U+2029 — so the
16
+ * body cannot close its own `</script>` tag or terminate a JS statement. This
17
+ * is the same sanitizer shape one ships as `utils/htmlEscape.safeJsonStringify`
18
+ * for the server-context payload. React offers no other way to emit a script
19
+ * body: text children of `<script>` are HTML-escaped, which would corrupt the
20
+ * JSON quoting.
21
+ *
22
+ * Ordering: an inline classic script in `<head>` runs during document parse,
23
+ * while the app bundle is a deferred module script — so `globalThis` already
24
+ * carries the payload by the time `@multiplatform.one/platform` constructs its
25
+ * `Config`.
26
+ *
27
+ * On the browser the body is empty: `serializeRuntimePublicConfig()` reads
28
+ * `process.env`, which only exists on the server. Rendering it client-side
29
+ * would publish an EMPTY payload over the real one.
30
+ */
31
+ export declare function RuntimePublicConfigScript(): import("react/jsx-runtime").JSX.Element;
32
+ export declare namespace RuntimePublicConfigScript {
33
+ var displayName: string;
34
+ }
35
+ //# sourceMappingURL=RuntimePublicConfigScript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RuntimePublicConfigScript.d.ts","sourceRoot":"","sources":["../../src/app/RuntimePublicConfigScript.tsx"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,yBAAyB,4CAgBxC;yBAhBe,yBAAyB"}