@multiplatform.one/platform 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.
@@ -25,13 +25,14 @@ __export(config_exports, {
25
25
  Config: () => Config
26
26
  });
27
27
  module.exports = __toCommonJS(config_exports);
28
- const import_meta = {};
28
+ var import_runtimeConfig = require("./runtimeConfig.cjs");
29
29
  class Config {
30
30
  _config = {};
31
31
  _blacklist = /* @__PURE__ */new Set([]);
32
32
  _lookupEnv(key) {
33
33
  if (this._blacklist.has(key)) return void 0;
34
- return process?.env?.[key];
34
+ if (typeof process === "undefined") return void 0;
35
+ return process.env?.[key];
35
36
  }
36
37
  _resolveConfig() {
37
38
  return Object.keys(this._config).reduce((config, key) => {
@@ -47,13 +48,18 @@ class Config {
47
48
  }, {});
48
49
  }
49
50
  constructor(config = {}) {
50
- let viteConfig = {};
51
- try {
52
- const raw = typeof import_meta !== "undefined" && import_meta.env?.VITE_MP_CONFIG;
53
- viteConfig = JSON.parse(raw || "{}");
54
- } catch {}
55
51
  this._config = {
56
- ...this._reduceConfig(typeof viteConfig === "object" ? viteConfig : {}),
52
+ // 1. BUILD time: public config baked into the bundle via VITE_MP_CONFIG.
53
+ // Kept first (and kept working) so apps that bake their config in —
54
+ // native, GNOME, static web — are unchanged.
55
+ ...this._reduceConfig((0, import_runtimeConfig.bakedPublicConfig)()),
56
+ // 2. RUNTIME: the public payload the SSR document published into the
57
+ // browser (see runtimeConfig.ts). Wins over the bake so a redeployed
58
+ // container overrides a stale build-time value, and so an image built
59
+ // with an EMPTY build environment can still be configured per
60
+ // deployment. `process.env` is not reachable here.
61
+ ...this._reduceConfig((0, import_runtimeConfig.readRuntimePublicConfig)()),
62
+ // 3. Explicit values handed to the constructor always win.
57
63
  ...this._reduceConfig(config)
58
64
  };
59
65
  }
@@ -28,4 +28,5 @@ __export(config_exports, {
28
28
  module.exports = __toCommonJS(config_exports);
29
29
  var import_config = require("./config.cjs");
30
30
  __reExport(config_exports, require("./config.cjs"), module.exports);
31
+ __reExport(config_exports, require("./runtimeConfig.cjs"), module.exports);
31
32
  const config = new import_config.Config();
@@ -0,0 +1,100 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
21
+ value: true
22
+ }), mod);
23
+ var runtimeConfig_exports = {};
24
+ __export(runtimeConfig_exports, {
25
+ bakedConfigEnvKey: () => bakedConfigEnvKey,
26
+ bakedPublicConfig: () => bakedPublicConfig,
27
+ publicConfigKeys: () => publicConfigKeys,
28
+ publicConfigKeysEnvKey: () => publicConfigKeysEnvKey,
29
+ readRuntimePublicConfig: () => readRuntimePublicConfig,
30
+ resolvePublicConfig: () => resolvePublicConfig,
31
+ runtimePublicConfigKey: () => runtimePublicConfigKey,
32
+ serializeRuntimePublicConfig: () => serializeRuntimePublicConfig
33
+ });
34
+ module.exports = __toCommonJS(runtimeConfig_exports);
35
+ const import_meta = {};
36
+ const runtimePublicConfigKey = "__mp_public_config__";
37
+ const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
38
+ const bakedConfigEnvKey = "VITE_MP_CONFIG";
39
+ function importMetaEnv() {
40
+ try {
41
+ if (typeof import_meta === "undefined") return {};
42
+ return import_meta.env || {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+ function processEnv() {
48
+ try {
49
+ if (typeof process === "undefined") return {};
50
+ return process.env || {};
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+ function readBuildEnv(key) {
56
+ return importMetaEnv()[key] ?? processEnv()[key];
57
+ }
58
+ function parseJson(raw) {
59
+ if (!raw) return void 0;
60
+ try {
61
+ return JSON.parse(raw);
62
+ } catch {
63
+ return void 0;
64
+ }
65
+ }
66
+ function stringifyValues(source) {
67
+ return Object.entries(source).reduce((acc, [key, value]) => {
68
+ if (typeof value !== "undefined" && value !== null) acc[key] = String(value);
69
+ return acc;
70
+ }, {});
71
+ }
72
+ function publicConfigKeys() {
73
+ const parsed = parseJson(readBuildEnv(publicConfigKeysEnvKey));
74
+ if (!Array.isArray(parsed)) return [];
75
+ return parsed.filter(key => typeof key === "string" && key.length > 0);
76
+ }
77
+ function bakedPublicConfig() {
78
+ const parsed = parseJson(readBuildEnv(bakedConfigEnvKey));
79
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
80
+ return stringifyValues(parsed);
81
+ }
82
+ function resolvePublicConfig(env = processEnv()) {
83
+ const baked = bakedPublicConfig();
84
+ return publicConfigKeys().reduce((acc, key) => {
85
+ const value = env[key] ?? baked[key];
86
+ if (typeof value === "string" && value.length > 0) acc[key] = value;
87
+ return acc;
88
+ }, {});
89
+ }
90
+ function readRuntimePublicConfig() {
91
+ const raw = globalThis[runtimePublicConfigKey];
92
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
93
+ return stringifyValues(raw);
94
+ }
95
+ function safeJsonStringify(value) {
96
+ return JSON.stringify(value ?? {}).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
97
+ }
98
+ function serializeRuntimePublicConfig(config = resolvePublicConfig()) {
99
+ return `globalThis[${JSON.stringify(runtimePublicConfigKey)}]=${safeJsonStringify(config)};`;
100
+ }
@@ -1,9 +1,11 @@
1
+ import { bakedPublicConfig, readRuntimePublicConfig } from "./runtimeConfig.mjs";
1
2
  class Config {
2
3
  _config = {};
3
4
  _blacklist = /* @__PURE__ */new Set([]);
4
5
  _lookupEnv(key) {
5
6
  if (this._blacklist.has(key)) return void 0;
6
- return process?.env?.[key];
7
+ if (typeof process === "undefined") return void 0;
8
+ return process.env?.[key];
7
9
  }
8
10
  _resolveConfig() {
9
11
  return Object.keys(this._config).reduce((config, key) => {
@@ -19,13 +21,18 @@ class Config {
19
21
  }, {});
20
22
  }
21
23
  constructor(config = {}) {
22
- let viteConfig = {};
23
- try {
24
- const raw = typeof import.meta !== "undefined" && import.meta.env?.VITE_MP_CONFIG;
25
- viteConfig = JSON.parse(raw || "{}");
26
- } catch {}
27
24
  this._config = {
28
- ...this._reduceConfig(typeof viteConfig === "object" ? viteConfig : {}),
25
+ // 1. BUILD time: public config baked into the bundle via VITE_MP_CONFIG.
26
+ // Kept first (and kept working) so apps that bake their config in —
27
+ // native, GNOME, static web — are unchanged.
28
+ ...this._reduceConfig(bakedPublicConfig()),
29
+ // 2. RUNTIME: the public payload the SSR document published into the
30
+ // browser (see runtimeConfig.ts). Wins over the bake so a redeployed
31
+ // container overrides a stale build-time value, and so an image built
32
+ // with an EMPTY build environment can still be configured per
33
+ // deployment. `process.env` is not reachable here.
34
+ ...this._reduceConfig(readRuntimePublicConfig()),
35
+ // 3. Explicit values handed to the constructor always win.
29
36
  ...this._reduceConfig(config)
30
37
  };
31
38
  }
@@ -1 +1 @@
1
- {"version":3,"names":["Config","_config","_blacklist","Set","_lookupEnv","key","has","process","env","_resolveConfig","Object","keys","reduce","config","value","_reduceConfig","entries","toString","constructor","viteConfig","raw","import","meta","VITE_MP_CONFIG","JSON","parse","get","defaultValue","set","add","remove"],"sources":["../../../src/config/config.ts"],"sourcesContent":[null],"mappings":"AAEO,MAAMA,MAAA,CAA0B;EAC7BC,OAAA,GAAkC,CAAC;EAEnCC,UAAA,GAA0B,mBAAIC,GAAA,CAAI,EAAE;EAEpCC,WAAWC,GAAA,EAAiC;IAClD,IAAI,KAAKH,UAAA,CAAWI,GAAA,CAAID,GAAG,GAAG,OAAO;IACrC,OAAOE,OAAA,EAASC,GAAA,GAAMH,GAAG;EAC3B;EAEQI,eAAA,EAAiB;IACvB,OAAOC,MAAA,CAAOC,IAAA,CAAK,KAAKV,OAAO,EAAEW,MAAA,CAA+B,CAACC,MAAA,EAAQR,GAAA,KAAQ;MAC/E,MAAMS,KAAA,GAAQ,KAAKV,UAAA,CAAWC,GAAG,KAAK,KAAKJ,OAAA,CAAQI,GAAG;MACtD,IAAI,OAAOS,KAAA,KAAU,aAAaD,MAAA,CAAOR,GAAG,IAAIS,KAAA;MAChD,OAAOD,MAAA;IACT,GAAG,CAAC,CAAC;EACP;EAEQE,cAAcF,MAAA,GAA6C,CAAC,GAAG;IACrE,OAAOH,MAAA,CAAOM,OAAA,CAAQH,MAAM,EAAED,MAAA,CAC5B,CAACC,OAAA,EAAQ,CAACR,GAAA,EAAKS,KAAK,MAAoC;MACtD,IAAI,OAAOA,KAAA,KAAU,aAAaD,OAAA,CAAOR,GAAG,IAAIS,KAAA,CAAMG,QAAA,CAAS;MAC/D,OAAOJ,OAAA;IACT,GACA,CAAC,CACH;EACF;EAEAK,YAAYL,MAAA,GAA6C,CAAC,GAAG;IAC3D,IAAIM,UAAA,GAAiD,CAAC;IACtD,IAAI;MAEF,MAAMC,GAAA,GAAM,OAAOC,MAAA,CAAAC,IAAA,KAAgB,eAAeD,MAAA,CAAAC,IAAA,CAAYd,GAAA,EAAKe,cAAA;MACnEJ,UAAA,GAAaK,IAAA,CAAKC,KAAA,CAAML,GAAA,IAAO,IAAI;IACrC,QAAQ,CAAC;IACT,KAAKnB,OAAA,GAAU;MACb,GAAG,KAAKc,aAAA,CAAc,OAAOI,UAAA,KAAe,WAAWA,UAAA,GAAa,CAAC,CAAC;MACtE,GAAG,KAAKJ,aAAA,CAAcF,MAAM;IAC9B;EACF;EAKAa,IAAIrB,GAAA,EAAcsB,YAAA,EAAoE;IACpF,MAAMd,MAAA,GAAS,KAAKJ,cAAA,CAAe;IACnC,IAAI,CAACJ,GAAA,EAAK,OAAOQ,MAAA;IACjB,OAAOA,MAAA,CAAOR,GAAG,KAAKsB,YAAA;EACxB;EAEAC,IAAIvB,GAAA,EAAaS,KAAA,EAAe;IAC9B,KAAKb,OAAA,CAAQI,GAAG,IAAIS,KAAA;IACpB,OAAOA,KAAA;EACT;EAEAe,IAAIf,KAAA,EAA+B;IACjC,KAAKb,OAAA,GAAU;MACb,GAAG,KAAKA,OAAA;MACR,GAAGa;IACL;IACA,OAAO,KAAKY,GAAA,CAAI;EAClB;EAEAI,OAAOzB,GAAA,EAAiC;IACtC,MAAMS,KAAA,GAAQ,KAAKY,GAAA,CAAIrB,GAAG;IAC1B,OAAO,KAAKJ,OAAA,CAAQI,GAAG;IACvB,OAAOS,KAAA;EACT;AACF","ignoreList":[]}
1
+ {"version":3,"names":["bakedPublicConfig","readRuntimePublicConfig","Config","_config","_blacklist","Set","_lookupEnv","key","has","process","env","_resolveConfig","Object","keys","reduce","config","value","_reduceConfig","entries","toString","constructor","get","defaultValue","set","add","remove"],"sources":["../../../src/config/config.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,iBAAA,EAAmBC,uBAAA,QAA+B;AAGpD,MAAMC,MAAA,CAA0B;EAC7BC,OAAA,GAAkC,CAAC;EAEnCC,UAAA,GAA0B,mBAAIC,GAAA,CAAI,EAAE;EAEpCC,WAAWC,GAAA,EAAiC;IAClD,IAAI,KAAKH,UAAA,CAAWI,GAAA,CAAID,GAAG,GAAG,OAAO;IAIrC,IAAI,OAAOE,OAAA,KAAY,aAAa,OAAO;IAC3C,OAAOA,OAAA,CAAQC,GAAA,GAAMH,GAAG;EAC1B;EAEQI,eAAA,EAAiB;IACvB,OAAOC,MAAA,CAAOC,IAAA,CAAK,KAAKV,OAAO,EAAEW,MAAA,CAA+B,CAACC,MAAA,EAAQR,GAAA,KAAQ;MAC/E,MAAMS,KAAA,GAAQ,KAAKV,UAAA,CAAWC,GAAG,KAAK,KAAKJ,OAAA,CAAQI,GAAG;MACtD,IAAI,OAAOS,KAAA,KAAU,aAAaD,MAAA,CAAOR,GAAG,IAAIS,KAAA;MAChD,OAAOD,MAAA;IACT,GAAG,CAAC,CAAC;EACP;EAEQE,cAAcF,MAAA,GAA6C,CAAC,GAAG;IACrE,OAAOH,MAAA,CAAOM,OAAA,CAAQH,MAAM,EAAED,MAAA,CAC5B,CAACC,OAAA,EAAQ,CAACR,GAAA,EAAKS,KAAK,MAAoC;MACtD,IAAI,OAAOA,KAAA,KAAU,aAAaD,OAAA,CAAOR,GAAG,IAAIS,KAAA,CAAMG,QAAA,CAAS;MAC/D,OAAOJ,OAAA;IACT,GACA,CAAC,CACH;EACF;EAEAK,YAAYL,MAAA,GAA6C,CAAC,GAAG;IAC3D,KAAKZ,OAAA,GAAU;MAAA;MAAA;MAAA;MAIb,GAAG,KAAKc,aAAA,CAAcjB,iBAAA,CAAkB,CAAC;MAAA;MAAA;MAAA;MAAA;MAAA;MAMzC,GAAG,KAAKiB,aAAA,CAAchB,uBAAA,CAAwB,CAAC;MAAA;MAE/C,GAAG,KAAKgB,aAAA,CAAcF,MAAM;IAC9B;EACF;EAKAM,IAAId,GAAA,EAAce,YAAA,EAAoE;IACpF,MAAMP,MAAA,GAAS,KAAKJ,cAAA,CAAe;IACnC,IAAI,CAACJ,GAAA,EAAK,OAAOQ,MAAA;IACjB,OAAOA,MAAA,CAAOR,GAAG,KAAKe,YAAA;EACxB;EAEAC,IAAIhB,GAAA,EAAaS,KAAA,EAAe;IAC9B,KAAKb,OAAA,CAAQI,GAAG,IAAIS,KAAA;IACpB,OAAOA,KAAA;EACT;EAEAQ,IAAIR,KAAA,EAA+B;IACjC,KAAKb,OAAA,GAAU;MACb,GAAG,KAAKA,OAAA;MACR,GAAGa;IACL;IACA,OAAO,KAAKK,GAAA,CAAI;EAClB;EAEAI,OAAOlB,GAAA,EAAiC;IACtC,MAAMS,KAAA,GAAQ,KAAKK,GAAA,CAAId,GAAG;IAC1B,OAAO,KAAKJ,OAAA,CAAQI,GAAG;IACvB,OAAOS,KAAA;EACT;AACF","ignoreList":[]}
@@ -1,5 +1,6 @@
1
1
  import { Config } from "./config.mjs";
2
2
  export * from "./config.mjs";
3
+ export * from "./runtimeConfig.mjs";
3
4
  const config = new Config();
4
5
  export { config };
5
6
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"names":["Config","config"],"sources":["../../../src/config/index.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,MAAA,QAAc;AAKvB,cAAc;AAHd,MAAMC,MAAA,GAAS,IAAID,MAAA,CAAO","ignoreList":[]}
1
+ {"version":3,"names":["Config","config"],"sources":["../../../src/config/index.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,MAAA,QAAc;AAKvB,cAAc;AACd,cAAc;AAJd,MAAMC,MAAA,GAAS,IAAID,MAAA,CAAO","ignoreList":[]}
@@ -0,0 +1,67 @@
1
+ const runtimePublicConfigKey = "__mp_public_config__";
2
+ const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
3
+ const bakedConfigEnvKey = "VITE_MP_CONFIG";
4
+ function importMetaEnv() {
5
+ try {
6
+ if (typeof import.meta === "undefined") return {};
7
+ return import.meta.env || {};
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+ function processEnv() {
13
+ try {
14
+ if (typeof process === "undefined") return {};
15
+ return process.env || {};
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+ function readBuildEnv(key) {
21
+ return importMetaEnv()[key] ?? processEnv()[key];
22
+ }
23
+ function parseJson(raw) {
24
+ if (!raw) return void 0;
25
+ try {
26
+ return JSON.parse(raw);
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ }
31
+ function stringifyValues(source) {
32
+ return Object.entries(source).reduce((acc, [key, value]) => {
33
+ if (typeof value !== "undefined" && value !== null) acc[key] = String(value);
34
+ return acc;
35
+ }, {});
36
+ }
37
+ function publicConfigKeys() {
38
+ const parsed = parseJson(readBuildEnv(publicConfigKeysEnvKey));
39
+ if (!Array.isArray(parsed)) return [];
40
+ return parsed.filter(key => typeof key === "string" && key.length > 0);
41
+ }
42
+ function bakedPublicConfig() {
43
+ const parsed = parseJson(readBuildEnv(bakedConfigEnvKey));
44
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
45
+ return stringifyValues(parsed);
46
+ }
47
+ function resolvePublicConfig(env = processEnv()) {
48
+ const baked = bakedPublicConfig();
49
+ return publicConfigKeys().reduce((acc, key) => {
50
+ const value = env[key] ?? baked[key];
51
+ if (typeof value === "string" && value.length > 0) acc[key] = value;
52
+ return acc;
53
+ }, {});
54
+ }
55
+ function readRuntimePublicConfig() {
56
+ const raw = globalThis[runtimePublicConfigKey];
57
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
58
+ return stringifyValues(raw);
59
+ }
60
+ function safeJsonStringify(value) {
61
+ return JSON.stringify(value ?? {}).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
62
+ }
63
+ function serializeRuntimePublicConfig(config = resolvePublicConfig()) {
64
+ return `globalThis[${JSON.stringify(runtimePublicConfigKey)}]=${safeJsonStringify(config)};`;
65
+ }
66
+ export { bakedConfigEnvKey, bakedPublicConfig, publicConfigKeys, publicConfigKeysEnvKey, readRuntimePublicConfig, resolvePublicConfig, runtimePublicConfigKey, serializeRuntimePublicConfig };
67
+ //# sourceMappingURL=runtimeConfig.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["runtimePublicConfigKey","publicConfigKeysEnvKey","bakedConfigEnvKey","importMetaEnv","import","meta","env","processEnv","process","readBuildEnv","key","parseJson","raw","JSON","parse","stringifyValues","source","Object","entries","reduce","acc","value","String","publicConfigKeys","parsed","Array","isArray","filter","length","bakedPublicConfig","resolvePublicConfig","baked","readRuntimePublicConfig","globalThis","safeJsonStringify","stringify","replace","serializeRuntimePublicConfig","config"],"sources":["../../../src/config/runtimeConfig.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/platform",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "Platform detection flags and utilities for multiplatform.one",
5
5
  "keywords": [
6
6
  "detection",
@@ -51,9 +51,12 @@
51
51
  "@types/chrome": "^0.1.40",
52
52
  "@types/firefox-webext-browser": "^143.0.0",
53
53
  "@types/react": "~19.2.14",
54
+ "@vitest/coverage-v8": "^4.1.5",
55
+ "happy-dom": "^20.9.0",
54
56
  "react": "19.2.5",
55
57
  "typescript": "~5.9.3",
56
- "@multiplatform.one/config": "7.0.0"
58
+ "vitest": "^4.1.5",
59
+ "@multiplatform.one/config": "7.1.0"
57
60
  },
58
61
  "peerDependencies": {
59
62
  "expo-constants": "~55.0.15",
@@ -1,9 +1,23 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { Config } from "./config";
3
+ import { bakedConfigEnvKey, runtimePublicConfigKey } from "./runtimeConfig";
4
+
5
+ function setRuntimePayload(payload: Record<string, string>) {
6
+ (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey] = payload;
7
+ }
8
+
9
+ function clearRuntimePayload() {
10
+ delete (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey];
11
+ }
3
12
 
4
13
  describe("Config", () => {
5
14
  beforeEach(() => {
6
15
  vi.unstubAllEnvs();
16
+ clearRuntimePayload();
17
+ });
18
+
19
+ afterEach(() => {
20
+ clearRuntimePayload();
7
21
  });
8
22
 
9
23
  describe("constructor", () => {
@@ -130,4 +144,51 @@ describe("Config", () => {
130
144
  expect(c.get("ADDED_KEY")).toBe("env-wins");
131
145
  });
132
146
  });
147
+
148
+ describe("runtime public config published by the SSR document", () => {
149
+ it("resolves MARKETPLACE_API_URL from the runtime payload with no build-time VITE_MP_CONFIG", () => {
150
+ // The exact production shape: an image built by CI with an EMPTY build
151
+ // environment (no VITE_MP_CONFIG baked), configured per deployment by
152
+ // the container env, which the SSR document publishes to the browser.
153
+ setRuntimePayload({ MARKETPLACE_API_URL: "https://api.marketplace.example.org" });
154
+ const c = new Config();
155
+ expect(c.get("MARKETPLACE_API_URL")).toBe("https://api.marketplace.example.org");
156
+ });
157
+
158
+ it("resolves FRAPPE_ENABLED from the runtime payload so the frappe provider can mount", () => {
159
+ // CreateApp.tsx gates FrappeProvider on config.get("FRAPPE_ENABLED") === "1".
160
+ setRuntimePayload({ FRAPPE_ENABLED: "1" });
161
+ const c = new Config();
162
+ expect(c.get("FRAPPE_ENABLED")).toBe("1");
163
+ });
164
+
165
+ // These use MARKETPLACE_API_URL rather than BASE_URL on purpose: vite (and
166
+ // therefore vitest) publishes its own BASE_URL="/" into process.env, and
167
+ // _lookupEnv still outranks everything on the SERVER — so a BASE_URL
168
+ // fixture would measure vite's value, not the precedence under test.
169
+ it("prefers the runtime payload over a stale build-time baked value", () => {
170
+ vi.stubEnv(
171
+ bakedConfigEnvKey,
172
+ JSON.stringify({ MARKETPLACE_API_URL: "https://stale.example.org" }),
173
+ );
174
+ setRuntimePayload({ MARKETPLACE_API_URL: "https://fresh.example.org" });
175
+ const c = new Config();
176
+ expect(c.get("MARKETPLACE_API_URL")).toBe("https://fresh.example.org");
177
+ });
178
+
179
+ it("still resolves the build-time baked value when no runtime payload exists", () => {
180
+ vi.stubEnv(
181
+ bakedConfigEnvKey,
182
+ JSON.stringify({ MARKETPLACE_API_URL: "https://baked.example.org" }),
183
+ );
184
+ const c = new Config();
185
+ expect(c.get("MARKETPLACE_API_URL")).toBe("https://baked.example.org");
186
+ });
187
+
188
+ it("lets explicit constructor values win over the runtime payload", () => {
189
+ setRuntimePayload({ MARKETPLACE_API_URL: "https://runtime.example.org" });
190
+ const c = new Config({ MARKETPLACE_API_URL: "https://explicit.example.org" });
191
+ expect(c.get("MARKETPLACE_API_URL")).toBe("https://explicit.example.org");
192
+ });
193
+ });
133
194
  });
@@ -1,3 +1,4 @@
1
+ import { bakedPublicConfig, readRuntimePublicConfig } from "./runtimeConfig";
1
2
  import type { IConfig } from "./types";
2
3
 
3
4
  export class Config implements IConfig {
@@ -7,7 +8,11 @@ export class Config implements IConfig {
7
8
 
8
9
  private _lookupEnv(key: string): string | undefined {
9
10
  if (this._blacklist.has(key)) return undefined;
10
- return process?.env?.[key];
11
+ // `process` is UNDECLARED in the browser, where `process?.env` throws a
12
+ // ReferenceError rather than short-circuiting — this is why env lookup
13
+ // could never be the browser's only path to config.
14
+ if (typeof process === "undefined") return undefined;
15
+ return process.env?.[key];
11
16
  }
12
17
 
13
18
  private _resolveConfig() {
@@ -29,14 +34,18 @@ export class Config implements IConfig {
29
34
  }
30
35
 
31
36
  constructor(config: Record<string, string | undefined> = {}) {
32
- let viteConfig: Record<string, string | undefined> = {};
33
- try {
34
- // Read public config injected by Vite at build time via VITE_MP_CONFIG
35
- const raw = typeof import.meta !== "undefined" && import.meta.env?.VITE_MP_CONFIG;
36
- viteConfig = JSON.parse(raw || "{}");
37
- } catch {}
38
37
  this._config = {
39
- ...this._reduceConfig(typeof viteConfig === "object" ? viteConfig : {}),
38
+ // 1. BUILD time: public config baked into the bundle via VITE_MP_CONFIG.
39
+ // Kept first (and kept working) so apps that bake their config in —
40
+ // native, GNOME, static web — are unchanged.
41
+ ...this._reduceConfig(bakedPublicConfig()),
42
+ // 2. RUNTIME: the public payload the SSR document published into the
43
+ // browser (see runtimeConfig.ts). Wins over the bake so a redeployed
44
+ // container overrides a stale build-time value, and so an image built
45
+ // with an EMPTY build environment can still be configured per
46
+ // deployment. `process.env` is not reachable here.
47
+ ...this._reduceConfig(readRuntimePublicConfig()),
48
+ // 3. Explicit values handed to the constructor always win.
40
49
  ...this._reduceConfig(config),
41
50
  };
42
51
  }
@@ -4,3 +4,4 @@ const config = new Config();
4
4
 
5
5
  export { config };
6
6
  export * from "./config";
7
+ export * from "./runtimeConfig";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Native twin of `runtimeConfig.ts`.
3
+ *
4
+ * Identical API, minus `import.meta`: the vxrn/rolldown native bundle is a
5
+ * CLASSIC script for Hermes, where `import.meta` is a syntax error (the same
6
+ * reason `config.native.ts` exists as a hand-written twin of `config.ts`).
7
+ *
8
+ * Native has no SSR document, so `readRuntimePublicConfig` normally returns
9
+ * undefined and native keeps resolving through `expo-constants` +
10
+ * `process.env` exactly as before. The exports are kept so that shared code
11
+ * importing from `@multiplatform.one/platform` compiles on every target.
12
+ */
13
+
14
+ export const runtimePublicConfigKey = "__mp_public_config__";
15
+ export const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
16
+ export const bakedConfigEnvKey = "VITE_MP_CONFIG";
17
+
18
+ function processEnv(): Record<string, string | undefined> {
19
+ try {
20
+ if (typeof process === "undefined") return {};
21
+ return (process.env || {}) as Record<string, string | undefined>;
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function parseJson(raw: string | undefined): unknown {
28
+ if (!raw) return undefined;
29
+ try {
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ function stringifyValues(source: Record<string, unknown>): Record<string, string> {
37
+ return Object.entries(source).reduce<Record<string, string>>((acc, [key, value]) => {
38
+ if (typeof value !== "undefined" && value !== null) acc[key] = String(value);
39
+ return acc;
40
+ }, {});
41
+ }
42
+
43
+ export function publicConfigKeys(): string[] {
44
+ const parsed = parseJson(processEnv()[publicConfigKeysEnvKey]);
45
+ if (!Array.isArray(parsed)) return [];
46
+ return parsed.filter((key): key is string => typeof key === "string" && key.length > 0);
47
+ }
48
+
49
+ export function bakedPublicConfig(): Record<string, string> {
50
+ const parsed = parseJson(processEnv()[bakedConfigEnvKey]);
51
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
52
+ return stringifyValues(parsed as Record<string, unknown>);
53
+ }
54
+
55
+ export function resolvePublicConfig(
56
+ env: Record<string, string | undefined> = processEnv(),
57
+ ): Record<string, string> {
58
+ const baked = bakedPublicConfig();
59
+ return publicConfigKeys().reduce<Record<string, string>>((acc, key) => {
60
+ const value = env[key] ?? baked[key];
61
+ if (typeof value === "string" && value.length > 0) acc[key] = value;
62
+ return acc;
63
+ }, {});
64
+ }
65
+
66
+ export function readRuntimePublicConfig(): Record<string, string> | undefined {
67
+ const raw = (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey];
68
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
69
+ return stringifyValues(raw as Record<string, unknown>);
70
+ }
71
+
72
+ function safeJsonStringify(value: unknown): string {
73
+ return JSON.stringify(value ?? {})
74
+ .replace(/</g, "\\u003c")
75
+ .replace(/>/g, "\\u003e")
76
+ .replace(/\u2028/g, "\\u2028")
77
+ .replace(/\u2029/g, "\\u2029");
78
+ }
79
+
80
+ export function serializeRuntimePublicConfig(
81
+ config: Record<string, string> = resolvePublicConfig(),
82
+ ): string {
83
+ return `globalThis[${JSON.stringify(runtimePublicConfigKey)}]=${safeJsonStringify(config)};`;
84
+ }
@@ -0,0 +1,128 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ bakedConfigEnvKey,
4
+ publicConfigKeysEnvKey,
5
+ readRuntimePublicConfig,
6
+ resolvePublicConfig,
7
+ runtimePublicConfigKey,
8
+ serializeRuntimePublicConfig,
9
+ } from "./runtimeConfig";
10
+
11
+ function stubAllowlist(keys: string[]) {
12
+ vi.stubEnv(publicConfigKeysEnvKey, JSON.stringify(keys));
13
+ }
14
+
15
+ function stubBaked(config: Record<string, string>) {
16
+ vi.stubEnv(bakedConfigEnvKey, JSON.stringify(config));
17
+ }
18
+
19
+ function clearRuntimePayload() {
20
+ delete (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey];
21
+ }
22
+
23
+ afterEach(() => {
24
+ vi.unstubAllEnvs();
25
+ clearRuntimePayload();
26
+ });
27
+
28
+ describe("resolvePublicConfig", () => {
29
+ it("resolves a public key from the RUNTIME environment, not the build", () => {
30
+ stubAllowlist(["MARKETPLACE_API_URL"]);
31
+ expect(
32
+ resolvePublicConfig({ MARKETPLACE_API_URL: "https://api.marketplace.example.org" }),
33
+ ).toEqual({
34
+ MARKETPLACE_API_URL: "https://api.marketplace.example.org",
35
+ });
36
+ });
37
+
38
+ it("lets the runtime environment override a stale baked value", () => {
39
+ stubAllowlist(["MARKETPLACE_API_URL"]);
40
+ stubBaked({ MARKETPLACE_API_URL: "https://stale.example.org" });
41
+ expect(resolvePublicConfig({ MARKETPLACE_API_URL: "https://fresh.example.org" })).toEqual({
42
+ MARKETPLACE_API_URL: "https://fresh.example.org",
43
+ });
44
+ });
45
+
46
+ it("falls back to the baked value when the runtime environment is empty", () => {
47
+ stubAllowlist(["MARKETPLACE_API_URL"]);
48
+ stubBaked({ MARKETPLACE_API_URL: "https://baked.example.org" });
49
+ expect(resolvePublicConfig({})).toEqual({
50
+ MARKETPLACE_API_URL: "https://baked.example.org",
51
+ });
52
+ });
53
+
54
+ it("resolves nothing when the build baked no public allowlist", () => {
55
+ expect(resolvePublicConfig({ MARKETPLACE_API_URL: "https://api.example.org" })).toEqual({});
56
+ });
57
+ });
58
+
59
+ describe("public allowlist enforcement", () => {
60
+ it("never resolves a key that is absent from the build-time public allowlist", () => {
61
+ stubAllowlist(["MARKETPLACE_API_URL"]);
62
+ const resolved = resolvePublicConfig({
63
+ MARKETPLACE_API_URL: "https://api.example.org",
64
+ SECRET: "hunter2",
65
+ DATABASE_PASSWORD: "hunter2",
66
+ });
67
+ expect(resolved).toEqual({ MARKETPLACE_API_URL: "https://api.example.org" });
68
+ expect(resolved).not.toHaveProperty("SECRET");
69
+ expect(resolved).not.toHaveProperty("DATABASE_PASSWORD");
70
+ });
71
+
72
+ it("never serializes a private key into the document payload", () => {
73
+ stubAllowlist(["MARKETPLACE_API_URL", "FRAPPE_ENABLED"]);
74
+ const payload = serializeRuntimePublicConfig(
75
+ resolvePublicConfig({
76
+ MARKETPLACE_API_URL: "https://api.example.org",
77
+ FRAPPE_ENABLED: "1",
78
+ SECRET: "hunter2",
79
+ }),
80
+ );
81
+ expect(payload).toContain("MARKETPLACE_API_URL");
82
+ expect(payload).toContain("FRAPPE_ENABLED");
83
+ expect(payload).not.toContain("SECRET");
84
+ expect(payload).not.toContain("hunter2");
85
+ });
86
+ });
87
+
88
+ describe("serializeRuntimePublicConfig", () => {
89
+ it("assigns the payload to the runtime global key", () => {
90
+ stubAllowlist(["FRAPPE_ENABLED"]);
91
+ expect(serializeRuntimePublicConfig(resolvePublicConfig({ FRAPPE_ENABLED: "1" }))).toBe(
92
+ `globalThis["${runtimePublicConfigKey}"]={"FRAPPE_ENABLED":"1"};`,
93
+ );
94
+ });
95
+
96
+ it("escapes characters that could break out of the inline script", () => {
97
+ stubAllowlist(["BASE_URL"]);
98
+ const payload = serializeRuntimePublicConfig(
99
+ resolvePublicConfig({ BASE_URL: "</script><script>alert(1)</script>" }),
100
+ );
101
+ expect(payload).not.toContain("</script>");
102
+ expect(payload).toContain("\\u003c");
103
+ });
104
+
105
+ it("emits an empty object rather than throwing when nothing resolves", () => {
106
+ expect(serializeRuntimePublicConfig({})).toBe(`globalThis["${runtimePublicConfigKey}"]={};`);
107
+ });
108
+ });
109
+
110
+ describe("readRuntimePublicConfig", () => {
111
+ it("reads the payload the SSR document published", () => {
112
+ (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey] = {
113
+ MARKETPLACE_API_URL: "https://api.example.org",
114
+ };
115
+ expect(readRuntimePublicConfig()).toEqual({
116
+ MARKETPLACE_API_URL: "https://api.example.org",
117
+ });
118
+ });
119
+
120
+ it("returns undefined when no document published a payload", () => {
121
+ expect(readRuntimePublicConfig()).toBeUndefined();
122
+ });
123
+
124
+ it("ignores a payload that is not a plain object", () => {
125
+ (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey] = ["nope"];
126
+ expect(readRuntimePublicConfig()).toBeUndefined();
127
+ });
128
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * RUNTIME public-config seam: SSR document -> browser.
3
+ *
4
+ * Why this exists: `VITE_MP_CONFIG` is resolved by `@multiplatform.one/config`
5
+ * at BUILD time (createViteConfig sets `process.env.VITE_MP_CONFIG` from the
6
+ * build environment, and vite bakes it into `import.meta.env`). A container
7
+ * image built by CI therefore ships whatever the CI job happened to export —
8
+ * usually nothing — and no amount of runtime container env reaches the
9
+ * browser, because `Config._lookupEnv` reads `process.env`, which does not
10
+ * exist there. The measured failure: the marketplace storefront had
11
+ * MARKETPLACE_API_URL on its container and still rendered "MARKETPLACE_API_URL
12
+ * is not configured", issuing ZERO catalog requests.
13
+ *
14
+ * The fix keeps the image environment-agnostic: the BUILD bakes only the
15
+ * NAMES of the public keys (`VITE_MP_PUBLIC_CONFIG_KEYS`), and the SSR node
16
+ * process resolves their VALUES from its own `process.env` at request time and
17
+ * publishes them into the served document.
18
+ *
19
+ * Transport: one inline `<script>` in `<head>` assigning a `globalThis` key —
20
+ * the SAME mechanism one uses for `__one_server_context__` (see one's
21
+ * `server/ServerContextScript`), deliberately sitting ALONGSIDE it rather
22
+ * than being a second, differently-shaped invention.
23
+ *
24
+ * Safety: `resolvePublicConfig` walks the BUILD-TIME PUBLIC ALLOWLIST, never
25
+ * the environment. A key that is not on `config.json`'s `public` list can not
26
+ * be serialized even if it is present in the SSR process env, so server-only
27
+ * secrets never reach the document.
28
+ */
29
+
30
+ /**
31
+ * Global key the SSR document assigns the resolved public config to.
32
+ * Sibling of one's `__one_server_context__`.
33
+ */
34
+ export const runtimePublicConfigKey = "__mp_public_config__";
35
+
36
+ /**
37
+ * Build-time env key carrying the JSON array of PUBLIC config key names
38
+ * (written by `createViteConfig` in `@multiplatform.one/config/vite`).
39
+ * Only the NAMES are baked — values stay environment-agnostic.
40
+ */
41
+ export const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
42
+
43
+ /**
44
+ * Build-time env key carrying the JSON object of public config values
45
+ * resolved from the BUILD environment. Still honored so apps that bake their
46
+ * config in (native, GNOME, static web) keep working unchanged.
47
+ */
48
+ export const bakedConfigEnvKey = "VITE_MP_CONFIG";
49
+
50
+ function importMetaEnv(): Record<string, string | undefined> {
51
+ try {
52
+ if (typeof import.meta === "undefined") return {};
53
+ return (import.meta.env || {}) as Record<string, string | undefined>;
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ function processEnv(): Record<string, string | undefined> {
60
+ try {
61
+ // `process` is undefined in the browser; a bare `process?.env` would
62
+ // still throw ReferenceError on an undeclared identifier.
63
+ if (typeof process === "undefined") return {};
64
+ return (process.env || {}) as Record<string, string | undefined>;
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function readBuildEnv(key: string): string | undefined {
71
+ return importMetaEnv()[key] ?? processEnv()[key];
72
+ }
73
+
74
+ function parseJson(raw: string | undefined): unknown {
75
+ if (!raw) return undefined;
76
+ try {
77
+ return JSON.parse(raw);
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function stringifyValues(source: Record<string, unknown>): Record<string, string> {
84
+ return Object.entries(source).reduce<Record<string, string>>((acc, [key, value]) => {
85
+ if (typeof value !== "undefined" && value !== null) acc[key] = String(value);
86
+ return acc;
87
+ }, {});
88
+ }
89
+
90
+ /**
91
+ * The PUBLIC key allowlist baked at build time. Empty when the app was not
92
+ * built through `createViteConfig` (tests, storybook, plain node) — in which
93
+ * case nothing is publishable and the payload stays empty, which is the safe
94
+ * direction to fail.
95
+ */
96
+ export function publicConfigKeys(): string[] {
97
+ const parsed = parseJson(readBuildEnv(publicConfigKeysEnvKey));
98
+ if (!Array.isArray(parsed)) return [];
99
+ return parsed.filter((key): key is string => typeof key === "string" && key.length > 0);
100
+ }
101
+
102
+ /** Public config values baked into the bundle at BUILD time. */
103
+ export function bakedPublicConfig(): Record<string, string> {
104
+ const parsed = parseJson(readBuildEnv(bakedConfigEnvKey));
105
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
106
+ return stringifyValues(parsed as Record<string, unknown>);
107
+ }
108
+
109
+ /**
110
+ * Resolve the public config the SSR document should publish.
111
+ *
112
+ * Iterates the ALLOWLIST — never the environment — so a private key present
113
+ * in `env` can not reach the returned object. Runtime env wins over the
114
+ * build-time bake, so a redeployed container overrides a stale baked value.
115
+ */
116
+ export function resolvePublicConfig(
117
+ env: Record<string, string | undefined> = processEnv(),
118
+ ): Record<string, string> {
119
+ const baked = bakedPublicConfig();
120
+ return publicConfigKeys().reduce<Record<string, string>>((acc, key) => {
121
+ const value = env[key] ?? baked[key];
122
+ if (typeof value === "string" && value.length > 0) acc[key] = value;
123
+ return acc;
124
+ }, {});
125
+ }
126
+
127
+ /**
128
+ * Read the payload the SSR document published. Returns undefined anywhere the
129
+ * document never ran (native, SSR itself, tests).
130
+ */
131
+ export function readRuntimePublicConfig(): Record<string, string> | undefined {
132
+ const raw = (globalThis as unknown as Record<string, unknown>)[runtimePublicConfigKey];
133
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
134
+ return stringifyValues(raw as Record<string, unknown>);
135
+ }
136
+
137
+ /**
138
+ * HTML-safe JSON: an inline `<script>` body must not be able to close its own
139
+ * tag, and U+2028/U+2029 are line terminators to a JS parser but legal inside
140
+ * a JSON string.
141
+ */
142
+ function safeJsonStringify(value: unknown): string {
143
+ return JSON.stringify(value ?? {})
144
+ .replace(/</g, "\\u003c")
145
+ .replace(/>/g, "\\u003e")
146
+ .replace(/\u2028/g, "\\u2028")
147
+ .replace(/\u2029/g, "\\u2029");
148
+ }
149
+
150
+ /**
151
+ * The inline `<script>` body that publishes the public config to the browser.
152
+ * Runs during document parse, so it lands before the deferred app bundle
153
+ * constructs its `Config`.
154
+ */
155
+ export function serializeRuntimePublicConfig(
156
+ config: Record<string, string> = resolvePublicConfig(),
157
+ ): string {
158
+ return `globalThis[${JSON.stringify(runtimePublicConfigKey)}]=${safeJsonStringify(config)};`;
159
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,qBAAa,MAAO,YAAW,OAAO;IACpC,OAAO,CAAC,OAAO,CAA8B;IAE7C,OAAO,CAAC,UAAU,CAA4B;IAE9C,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;gBAUT,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM;IAa3D,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IACpC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAO9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAK9B,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQjC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAKxC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,qBAAa,MAAO,YAAW,OAAO;IACpC,OAAO,CAAC,OAAO,CAA8B;IAE7C,OAAO,CAAC,UAAU,CAA4B;IAE9C,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;gBAUT,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM;IAiB3D,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IACpC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAO9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAK9B,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQjC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAKxC"}
@@ -2,4 +2,5 @@ import { Config } from "./config";
2
2
  declare const config: Config;
3
3
  export { config };
4
4
  export * from "./config";
5
+ export * from "./runtimeConfig";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,QAAA,MAAM,MAAM,QAAe,CAAC;AAE5B,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,QAAA,MAAM,MAAM,QAAe,CAAC;AAE5B,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * RUNTIME public-config seam: SSR document -> browser.
3
+ *
4
+ * Why this exists: `VITE_MP_CONFIG` is resolved by `@multiplatform.one/config`
5
+ * at BUILD time (createViteConfig sets `process.env.VITE_MP_CONFIG` from the
6
+ * build environment, and vite bakes it into `import.meta.env`). A container
7
+ * image built by CI therefore ships whatever the CI job happened to export —
8
+ * usually nothing — and no amount of runtime container env reaches the
9
+ * browser, because `Config._lookupEnv` reads `process.env`, which does not
10
+ * exist there. The measured failure: the marketplace storefront had
11
+ * MARKETPLACE_API_URL on its container and still rendered "MARKETPLACE_API_URL
12
+ * is not configured", issuing ZERO catalog requests.
13
+ *
14
+ * The fix keeps the image environment-agnostic: the BUILD bakes only the
15
+ * NAMES of the public keys (`VITE_MP_PUBLIC_CONFIG_KEYS`), and the SSR node
16
+ * process resolves their VALUES from its own `process.env` at request time and
17
+ * publishes them into the served document.
18
+ *
19
+ * Transport: one inline `<script>` in `<head>` assigning a `globalThis` key —
20
+ * the SAME mechanism one uses for `__one_server_context__` (see one's
21
+ * `server/ServerContextScript`), deliberately sitting ALONGSIDE it rather
22
+ * than being a second, differently-shaped invention.
23
+ *
24
+ * Safety: `resolvePublicConfig` walks the BUILD-TIME PUBLIC ALLOWLIST, never
25
+ * the environment. A key that is not on `config.json`'s `public` list can not
26
+ * be serialized even if it is present in the SSR process env, so server-only
27
+ * secrets never reach the document.
28
+ */
29
+ /**
30
+ * Global key the SSR document assigns the resolved public config to.
31
+ * Sibling of one's `__one_server_context__`.
32
+ */
33
+ export declare const runtimePublicConfigKey = "__mp_public_config__";
34
+ /**
35
+ * Build-time env key carrying the JSON array of PUBLIC config key names
36
+ * (written by `createViteConfig` in `@multiplatform.one/config/vite`).
37
+ * Only the NAMES are baked — values stay environment-agnostic.
38
+ */
39
+ export declare const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
40
+ /**
41
+ * Build-time env key carrying the JSON object of public config values
42
+ * resolved from the BUILD environment. Still honored so apps that bake their
43
+ * config in (native, GNOME, static web) keep working unchanged.
44
+ */
45
+ export declare const bakedConfigEnvKey = "VITE_MP_CONFIG";
46
+ /**
47
+ * The PUBLIC key allowlist baked at build time. Empty when the app was not
48
+ * built through `createViteConfig` (tests, storybook, plain node) — in which
49
+ * case nothing is publishable and the payload stays empty, which is the safe
50
+ * direction to fail.
51
+ */
52
+ export declare function publicConfigKeys(): string[];
53
+ /** Public config values baked into the bundle at BUILD time. */
54
+ export declare function bakedPublicConfig(): Record<string, string>;
55
+ /**
56
+ * Resolve the public config the SSR document should publish.
57
+ *
58
+ * Iterates the ALLOWLIST — never the environment — so a private key present
59
+ * in `env` can not reach the returned object. Runtime env wins over the
60
+ * build-time bake, so a redeployed container overrides a stale baked value.
61
+ */
62
+ export declare function resolvePublicConfig(env?: Record<string, string | undefined>): Record<string, string>;
63
+ /**
64
+ * Read the payload the SSR document published. Returns undefined anywhere the
65
+ * document never ran (native, SSR itself, tests).
66
+ */
67
+ export declare function readRuntimePublicConfig(): Record<string, string> | undefined;
68
+ /**
69
+ * The inline `<script>` body that publishes the public config to the browser.
70
+ * Runs during document parse, so it lands before the deferred app bundle
71
+ * constructs its `Config`.
72
+ */
73
+ export declare function serializeRuntimePublicConfig(config?: Record<string, string>): string;
74
+ //# sourceMappingURL=runtimeConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtimeConfig.d.ts","sourceRoot":"","sources":["../../src/config/runtimeConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,+BAA+B,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AA0ClD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAI3C;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAgB,GACrD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAI5E;AAeD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAyB,GACrD,MAAM,CAER"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Native twin of `runtimeConfig.ts`.
3
+ *
4
+ * Identical API, minus `import.meta`: the vxrn/rolldown native bundle is a
5
+ * CLASSIC script for Hermes, where `import.meta` is a syntax error (the same
6
+ * reason `config.native.ts` exists as a hand-written twin of `config.ts`).
7
+ *
8
+ * Native has no SSR document, so `readRuntimePublicConfig` normally returns
9
+ * undefined and native keeps resolving through `expo-constants` +
10
+ * `process.env` exactly as before. The exports are kept so that shared code
11
+ * importing from `@multiplatform.one/platform` compiles on every target.
12
+ */
13
+ export declare const runtimePublicConfigKey = "__mp_public_config__";
14
+ export declare const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
15
+ export declare const bakedConfigEnvKey = "VITE_MP_CONFIG";
16
+ export declare function publicConfigKeys(): string[];
17
+ export declare function bakedPublicConfig(): Record<string, string>;
18
+ export declare function resolvePublicConfig(env?: Record<string, string | undefined>): Record<string, string>;
19
+ export declare function readRuntimePublicConfig(): Record<string, string> | undefined;
20
+ export declare function serializeRuntimePublicConfig(config?: Record<string, string>): string;
21
+ //# sourceMappingURL=runtimeConfig.native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtimeConfig.native.d.ts","sourceRoot":"","sources":["../../src/config/runtimeConfig.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAC7D,eAAO,MAAM,sBAAsB,+BAA+B,CAAC;AACnE,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AA2BlD,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAI3C;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1D;AAED,wBAAgB,mBAAmB,CACjC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAgB,GACrD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAI5E;AAUD,wBAAgB,4BAA4B,CAC1C,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAyB,GACrD,MAAM,CAER"}