@volley/vwr-loader 1.0.0-alpha.1

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.
Files changed (54) hide show
  1. package/README.md +205 -0
  2. package/dist/amplitudeFlagFetcher.d.ts +23 -0
  3. package/dist/amplitudeFlagFetcher.d.ts.map +1 -0
  4. package/dist/amplitudeFlagFetcher.js +60 -0
  5. package/dist/amplitudeFlagFetcher.js.map +1 -0
  6. package/dist/cli.js +177 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/envDefaults.d.ts +9 -0
  9. package/dist/envDefaults.d.ts.map +1 -0
  10. package/dist/envDefaults.js +36 -0
  11. package/dist/envDefaults.js.map +1 -0
  12. package/dist/getDeviceId.d.ts +65 -0
  13. package/dist/getDeviceId.d.ts.map +1 -0
  14. package/dist/getDeviceId.js +196 -0
  15. package/dist/getDeviceId.js.map +1 -0
  16. package/dist/getShellVersion.d.ts +34 -0
  17. package/dist/getShellVersion.d.ts.map +1 -0
  18. package/dist/getShellVersion.js +84 -0
  19. package/dist/getShellVersion.js.map +1 -0
  20. package/dist/index.d.ts +9 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.html +25 -0
  23. package/dist/index.js +6 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/loadVwr.d.ts +19 -0
  26. package/dist/loadVwr.d.ts.map +1 -0
  27. package/dist/loadVwr.js +104 -0
  28. package/dist/loadVwr.js.map +1 -0
  29. package/dist/logger.d.ts +7 -0
  30. package/dist/logger.d.ts.map +1 -0
  31. package/dist/logger.js +6 -0
  32. package/dist/logger.js.map +1 -0
  33. package/dist/main.js +2 -0
  34. package/dist/main.js.map +1 -0
  35. package/dist/vwrConfig.d.ts +19 -0
  36. package/dist/vwrConfig.d.ts.map +1 -0
  37. package/dist/vwrConfig.js +172 -0
  38. package/dist/vwrConfig.js.map +1 -0
  39. package/package.json +54 -0
  40. package/src/amplitudeFlagFetcher.test.ts +209 -0
  41. package/src/amplitudeFlagFetcher.ts +88 -0
  42. package/src/envDefaults.ts +45 -0
  43. package/src/getDeviceId.test.ts +237 -0
  44. package/src/getDeviceId.ts +243 -0
  45. package/src/getShellVersion.test.ts +278 -0
  46. package/src/getShellVersion.ts +114 -0
  47. package/src/index.html +25 -0
  48. package/src/index.ts +8 -0
  49. package/src/loadVwr.ts +126 -0
  50. package/src/logger.ts +14 -0
  51. package/src/main.ts +26 -0
  52. package/src/vite-env.d.ts +15 -0
  53. package/src/vwrConfig.test.ts +316 -0
  54. package/src/vwrConfig.ts +293 -0
@@ -0,0 +1,316 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+
3
+ // Stub environment to use "production" which maps to ENV_DEFAULTS["production"]
4
+ vi.stubEnv("VITE_ENVIRONMENT", "production")
5
+
6
+ import type { VWRConfig, VWRConfigRequest } from "./vwrConfig"
7
+
8
+ describe("vwrConfig", async () => {
9
+ const { getVWRConfig, validateConfig } = await import("./vwrConfig")
10
+ describe("validateConfig", () => {
11
+ const config = {
12
+ vwrUrl: "https://vwr.example.com",
13
+ hubUrl: "https://hub.example.com",
14
+ launchUrl: "https://launch.example.com",
15
+ trustedDomains: ["example.com"],
16
+ }
17
+
18
+ it("returns false for empty vwrUrl", () => {
19
+ const invalidConfig = { ...config, vwrUrl: "" }
20
+ expect(validateConfig(invalidConfig)).toBe(false)
21
+ })
22
+
23
+ it("returns false for undefined vwrUrl", () => {
24
+ const invalidConfig: VWRConfig = JSON.parse(
25
+ JSON.stringify({ ...config, vwrUrl: undefined })
26
+ )
27
+ expect(validateConfig(invalidConfig)).toBe(false)
28
+ })
29
+
30
+ it("returns false for empty hubUrl", () => {
31
+ const invalidConfig = { ...config, hubUrl: "" }
32
+ expect(validateConfig(invalidConfig)).toBe(false)
33
+ })
34
+
35
+ it("returns false for undefined hubUrl", () => {
36
+ const invalidConfig: VWRConfig = JSON.parse(
37
+ JSON.stringify({ ...config, hubUrl: undefined })
38
+ )
39
+ expect(validateConfig(invalidConfig)).toBe(false)
40
+ })
41
+
42
+ it("returns true for empty launchUrl (optional field)", () => {
43
+ const validConfig = { ...config, launchUrl: "" }
44
+ expect(validateConfig(validConfig)).toBe(true)
45
+ })
46
+
47
+ it("returns true for undefined launchUrl (optional field)", () => {
48
+ const validConfig: VWRConfig = JSON.parse(
49
+ JSON.stringify({ ...config, launchUrl: undefined })
50
+ )
51
+ expect(validateConfig(validConfig)).toBe(true)
52
+ })
53
+
54
+ it("returns false for empty trustedDomains", () => {
55
+ const invalidConfig = { ...config, trustedDomains: [] }
56
+ expect(validateConfig(invalidConfig)).toBe(false)
57
+ })
58
+
59
+ it("returns false for undefined trustedDomains", () => {
60
+ const invalidConfig: VWRConfig = JSON.parse(
61
+ JSON.stringify({ ...config, trustedDomains: undefined })
62
+ )
63
+ expect(validateConfig(invalidConfig)).toBe(false)
64
+ })
65
+
66
+ it("returns false for non array trustedDomains", () => {
67
+ const invalidConfig: VWRConfig = JSON.parse(
68
+ JSON.stringify({ ...config, trustedDomains: "" })
69
+ )
70
+ expect(validateConfig(invalidConfig)).toBe(false)
71
+ })
72
+ })
73
+
74
+ describe("getVWRConfig", () => {
75
+ const configRequest: VWRConfigRequest = {
76
+ configUrl: "https://config.example.com",
77
+ configFile: "vwr-config.json",
78
+ platform: "web",
79
+ deviceId: "device-123",
80
+ environment: "production",
81
+ shellVersion: "1.0.0",
82
+ }
83
+
84
+ const fetchSpy = vi.spyOn(global, "fetch")
85
+
86
+ it("returns default config when no configs are found", async () => {
87
+ const config = await getVWRConfig(configRequest)
88
+ // Defaults come from ENV_DEFAULTS["production"]
89
+ expect(config.vwrUrl).toBe("https://vwr.volley.tv/v1/latest/vwr.js")
90
+ expect(config.hubUrl).toBe("https://game-clients.volley.tv/hub")
91
+ expect(config.launchUrl).toBeUndefined() // Optional, not set by default
92
+ expect(config.trustedDomains).toEqual([
93
+ "https://game-clients.volley.tv/hub",
94
+ "https://vwr.volley.tv/v1/latest/vwr.js",
95
+ ])
96
+ })
97
+
98
+ it("returns local config when available", async () => {
99
+ fetchSpy.mockResolvedValueOnce(
100
+ new Response(
101
+ JSON.stringify({
102
+ vwrUrl: "https://local-vwr.example.com",
103
+ hubUrl: "https://local-hub.example.com",
104
+ launchUrl: "https://local-launch.example.com",
105
+ trustedDomains: [
106
+ "https://local-hub.example.com",
107
+ "https://local-vwr.example.com",
108
+ "https://local-launch.example.com",
109
+ ],
110
+ })
111
+ )
112
+ )
113
+ fetchSpy.mockResolvedValueOnce(
114
+ new Response(
115
+ JSON.stringify({
116
+ vwrUrl: "https://device-vwr.example.com",
117
+ hubUrl: "https://device-hub.example.com",
118
+ launchUrl: "https://device-launch.example.com",
119
+ trustedDomains: [
120
+ "https://device-hub.example.com",
121
+ "https://device-vwr.example.com",
122
+ "https://device-launch.example.com",
123
+ ],
124
+ })
125
+ )
126
+ )
127
+ fetchSpy.mockResolvedValueOnce(
128
+ new Response(
129
+ JSON.stringify({
130
+ vwrUrl: "https://shell-vwr.example.com",
131
+ hubUrl: "https://shell-hub.example.com",
132
+ launchUrl: "https://shell-launch.example.com",
133
+ trustedDomains: [
134
+ "https://shell-hub.example.com",
135
+ "https://shell-vwr.example.com",
136
+ "https://shell-launch.example.com",
137
+ ],
138
+ })
139
+ )
140
+ )
141
+ fetchSpy.mockResolvedValueOnce(
142
+ new Response(
143
+ JSON.stringify({
144
+ vwrUrl: "https://environment-vwr.example.com",
145
+ hubUrl: "https://environment-hub.example.com",
146
+ launchUrl: "https://environment-launch.example.com",
147
+ trustedDomains: [
148
+ "https://environment-hub.example.com",
149
+ "https://environment-vwr.example.com",
150
+ "https://environment-launch.example.com",
151
+ ],
152
+ })
153
+ )
154
+ )
155
+ const config = await getVWRConfig(configRequest)
156
+ expect(validateConfig(config)).toBe(true)
157
+ expect(config.vwrUrl).toBe("https://local-vwr.example.com")
158
+ expect(config.hubUrl).toBe("https://local-hub.example.com")
159
+ expect(config.launchUrl).toBe("https://local-launch.example.com")
160
+ expect(config.trustedDomains).toEqual([
161
+ "https://local-hub.example.com",
162
+ "https://local-vwr.example.com",
163
+ "https://local-launch.example.com",
164
+ ])
165
+ })
166
+
167
+ it("returns device config when available", async () => {
168
+ fetchSpy.mockResolvedValueOnce(
169
+ new Response(
170
+ JSON.stringify({
171
+ vwrUrl: "https://device-vwr.example.com",
172
+ hubUrl: "https://device-hub.example.com",
173
+ launchUrl: "https://device-launch.example.com",
174
+ trustedDomains: [
175
+ "https://device-hub.example.com",
176
+ "https://device-vwr.example.com",
177
+ "https://device-launch.example.com",
178
+ ],
179
+ })
180
+ )
181
+ )
182
+ fetchSpy.mockResolvedValueOnce(
183
+ new Response(
184
+ JSON.stringify({
185
+ vwrUrl: "https://shell-vwr.example.com",
186
+ hubUrl: "https://shell-hub.example.com",
187
+ launchUrl: "https://shell-launch.example.com",
188
+ trustedDomains: [
189
+ "https://shell-hub.example.com",
190
+ "https://shell-vwr.example.com",
191
+ "https://shell-launch.example.com",
192
+ ],
193
+ })
194
+ )
195
+ )
196
+ fetchSpy.mockResolvedValueOnce(
197
+ new Response(
198
+ JSON.stringify({
199
+ vwrUrl: "https://environment-vwr.example.com",
200
+ hubUrl: "https://environment-hub.example.com",
201
+ launchUrl: "https://environment-launch.example.com",
202
+ trustedDomains: [
203
+ "https://environment-hub.example.com",
204
+ "https://environment-vwr.example.com",
205
+ "https://environment-launch.example.com",
206
+ ],
207
+ })
208
+ )
209
+ )
210
+ const config = await getVWRConfig(configRequest)
211
+ expect(validateConfig(config)).toBe(true)
212
+ expect(config.vwrUrl).toBe("https://device-vwr.example.com")
213
+ expect(config.hubUrl).toBe("https://device-hub.example.com")
214
+ expect(config.launchUrl).toBe("https://device-launch.example.com")
215
+ expect(config.trustedDomains).toEqual([
216
+ "https://device-hub.example.com",
217
+ "https://device-vwr.example.com",
218
+ "https://device-launch.example.com",
219
+ ])
220
+ })
221
+
222
+ it("returns shell config when available", async () => {
223
+ fetchSpy.mockResolvedValueOnce(
224
+ new Response(
225
+ JSON.stringify({
226
+ vwrUrl: "https://shell-vwr.example.com",
227
+ hubUrl: "https://shell-hub.example.com",
228
+ launchUrl: "https://shell-launch.example.com",
229
+ trustedDomains: [
230
+ "https://shell-hub.example.com",
231
+ "https://shell-vwr.example.com",
232
+ "https://shell-launch.example.com",
233
+ ],
234
+ })
235
+ )
236
+ )
237
+ fetchSpy.mockResolvedValueOnce(
238
+ new Response(
239
+ JSON.stringify({
240
+ vwrUrl: "https://environment-vwr.example.com",
241
+ hubUrl: "https://environment-hub.example.com",
242
+ launchUrl: "https://environment-launch.example.com",
243
+ trustedDomains: [
244
+ "https://environment-hub.example.com",
245
+ "https://environment-vwr.example.com",
246
+ "https://environment-launch.example.com",
247
+ ],
248
+ })
249
+ )
250
+ )
251
+ const config = await getVWRConfig(configRequest)
252
+ expect(validateConfig(config)).toBe(true)
253
+ expect(config.vwrUrl).toBe("https://shell-vwr.example.com")
254
+ expect(config.hubUrl).toBe("https://shell-hub.example.com")
255
+ expect(config.launchUrl).toBe("https://shell-launch.example.com")
256
+ expect(config.trustedDomains).toEqual([
257
+ "https://shell-hub.example.com",
258
+ "https://shell-vwr.example.com",
259
+ "https://shell-launch.example.com",
260
+ ])
261
+ })
262
+
263
+ it("returns environment config when available", async () => {
264
+ fetchSpy.mockResolvedValueOnce(
265
+ new Response(
266
+ JSON.stringify({
267
+ vwrUrl: "https://environment-vwr.example.com",
268
+ hubUrl: "https://environment-hub.example.com",
269
+ launchUrl: "https://environment-launch.example.com",
270
+ trustedDomains: [
271
+ "https://environment-hub.example.com",
272
+ "https://environment-vwr.example.com",
273
+ "https://environment-launch.example.com",
274
+ ],
275
+ })
276
+ )
277
+ )
278
+ const config = await getVWRConfig(configRequest)
279
+ expect(validateConfig(config)).toBe(true)
280
+ expect(config.vwrUrl).toBe("https://environment-vwr.example.com")
281
+ expect(config.hubUrl).toBe("https://environment-hub.example.com")
282
+ expect(config.launchUrl).toBe(
283
+ "https://environment-launch.example.com"
284
+ )
285
+ expect(config.trustedDomains).toEqual([
286
+ "https://environment-hub.example.com",
287
+ "https://environment-vwr.example.com",
288
+ "https://environment-launch.example.com",
289
+ ])
290
+ })
291
+
292
+ it("returns default config for missing values", async () => {
293
+ fetchSpy.mockResolvedValueOnce(
294
+ new Response(
295
+ JSON.stringify({
296
+ vwrUrl: "https://environment-vwr.example.com",
297
+ hubUrl: "https://environment-hub.example.com",
298
+ trustedDomains: [
299
+ "https://environment-hub.example.com",
300
+ "https://environment-vwr.example.com",
301
+ ],
302
+ })
303
+ )
304
+ )
305
+ const config = await getVWRConfig(configRequest)
306
+ expect(config.vwrUrl).toBe("https://environment-vwr.example.com")
307
+ expect(config.hubUrl).toBe("https://environment-hub.example.com")
308
+ // launchUrl is optional, not filled when missing
309
+ expect(config.launchUrl).toBeUndefined()
310
+ expect(config.trustedDomains).toEqual([
311
+ "https://environment-hub.example.com",
312
+ "https://environment-vwr.example.com",
313
+ ])
314
+ })
315
+ })
316
+ })
@@ -0,0 +1,293 @@
1
+ import { ENV_DEFAULTS } from "./envDefaults"
2
+ import { defaultLogger, type Logger } from "./logger"
3
+
4
+ export type VWRConfig = {
5
+ hubUrl: string
6
+ vwrUrl: string
7
+ launchUrl: string | undefined
8
+ trustedDomains: Array<string>
9
+ }
10
+
11
+ export type VWRConfigRequest = {
12
+ configUrl: string
13
+ configFile: string
14
+ platform: string
15
+ deviceId: string
16
+ environment: string
17
+ shellVersion: string
18
+ timeout?: number
19
+ }
20
+
21
+ const DEFAULT_CONFIG_TIMEOUT = 2000
22
+
23
+ /**
24
+ * Build a URL from a base and relative path, handling missing trailing slashes.
25
+ *
26
+ * The URL constructor resolves relative paths against the "directory" of the base URL,
27
+ * which is defined as everything up to (but not including) the last path segment.
28
+ * Without a trailing slash, the last segment is treated as a "file" and gets replaced.
29
+ *
30
+ * Example:
31
+ * new URL("a/b.json", "https://x.com/config") → "https://x.com/a/b.json" (wrong)
32
+ * new URL("a/b.json", "https://x.com/config/") → "https://x.com/config/a/b.json" (correct)
33
+ *
34
+ * This helper normalizes the base URL to ensure correct resolution.
35
+ */
36
+ const buildUrl = (path: string, base: string): URL => {
37
+ const normalizedBase = base.endsWith("/") ? base : `${base}/`
38
+ return new URL(path, normalizedBase)
39
+ }
40
+
41
+ export const getVWRConfig = async (
42
+ request: VWRConfigRequest,
43
+ logger: Logger = defaultLogger
44
+ ): Promise<VWRConfig> => {
45
+ const timeout = request.timeout ?? DEFAULT_CONFIG_TIMEOUT
46
+
47
+ logger.info(
48
+ "[VWR Config] Fetching config with priority: local → device → shellVersion → environment → defaults"
49
+ )
50
+
51
+ // Fetch all configs in parallel for performance
52
+ const [localConfig, deviceConfig, shellVersionConfig, environmentConfig] =
53
+ await Promise.all([
54
+ tryGetLocalConfig(request.configFile, timeout, logger),
55
+ tryGetDeviceConfig(
56
+ request.configUrl,
57
+ request.configFile,
58
+ request.platform,
59
+ request.deviceId,
60
+ timeout,
61
+ logger
62
+ ),
63
+ tryGetShellVersionConfig(
64
+ request.configUrl,
65
+ request.configFile,
66
+ request.environment,
67
+ request.platform,
68
+ request.shellVersion,
69
+ timeout,
70
+ logger
71
+ ),
72
+ tryGetEnvironmentConfig(
73
+ request.configUrl,
74
+ request.configFile,
75
+ request.environment,
76
+ timeout,
77
+ logger
78
+ ),
79
+ ])
80
+
81
+ // Return first successful config in priority order
82
+ if (localConfig !== null) {
83
+ logger.info("[VWR Config] ✓ Using config from: local", localConfig)
84
+ return localConfig
85
+ }
86
+ if (deviceConfig !== null) {
87
+ logger.info("[VWR Config] ✓ Using config from: device", deviceConfig)
88
+ return deviceConfig
89
+ }
90
+ if (shellVersionConfig !== null) {
91
+ logger.info(
92
+ "[VWR Config] ✓ Using config from: shellVersion",
93
+ shellVersionConfig
94
+ )
95
+ return shellVersionConfig
96
+ }
97
+ if (environmentConfig !== null) {
98
+ logger.info(
99
+ "[VWR Config] ✓ Using config from: environment",
100
+ environmentConfig
101
+ )
102
+ return environmentConfig
103
+ }
104
+
105
+ // All fetches failed, use defaults
106
+ logger.warn(
107
+ "[VWR Config] All config fetches failed, using built-in defaults"
108
+ )
109
+ const defaultConfig = getDefaultConfig()
110
+ return defaultConfig
111
+ }
112
+
113
+ const tryGetLocalConfig = async (
114
+ configFile: string,
115
+ timeout: number,
116
+ logger: Logger
117
+ ): Promise<VWRConfig | null> => {
118
+ try {
119
+ const url = buildUrl(configFile, ENV_DEFAULTS["local"]?.configUrl ?? "")
120
+ logger.info(`[VWR Config] Trying local: ${url}`)
121
+ return tryGetVWRConfig(url, timeout)
122
+ } catch (error) {
123
+ logger.error(
124
+ `[VWR Config] URL construction failed for local config: ${error}`
125
+ )
126
+ return null
127
+ }
128
+ }
129
+
130
+ const tryGetDeviceConfig = async (
131
+ configUrl: string,
132
+ configFile: string,
133
+ platform: string,
134
+ deviceId: string,
135
+ timeout: number,
136
+ logger: Logger
137
+ ): Promise<VWRConfig | null> => {
138
+ try {
139
+ const url = buildUrl(
140
+ `device/${platform}/${deviceId}/${configFile}`,
141
+ configUrl
142
+ )
143
+ logger.info(`[VWR Config] Trying device: ${url}`)
144
+ return tryGetVWRConfig(url, timeout)
145
+ } catch (error) {
146
+ logger.error(
147
+ `[VWR Config] URL construction failed for device config: ${error}`
148
+ )
149
+ return null
150
+ }
151
+ }
152
+
153
+ const tryGetEnvironmentConfig = async (
154
+ configUrl: string,
155
+ configFile: string,
156
+ environment: string,
157
+ timeout: number,
158
+ logger: Logger
159
+ ): Promise<VWRConfig | null> => {
160
+ try {
161
+ const url = buildUrl(
162
+ `environments/${environment}/${configFile}`,
163
+ configUrl
164
+ )
165
+ logger.info(`[VWR Config] Trying environment: ${url}`)
166
+ return tryGetVWRConfig(url, timeout)
167
+ } catch (error) {
168
+ logger.error(
169
+ `[VWR Config] URL construction failed for environment config: ${error}`
170
+ )
171
+ return null
172
+ }
173
+ }
174
+
175
+ const tryGetShellVersionConfig = async (
176
+ configUrl: string,
177
+ configFile: string,
178
+ environment: string,
179
+ platform: string,
180
+ shellVersion: string,
181
+ timeout: number,
182
+ logger: Logger
183
+ ): Promise<VWRConfig | null> => {
184
+ try {
185
+ const url = buildUrl(
186
+ `shellVersion/${environment}/${platform}/${shellVersion}/${configFile}`,
187
+ configUrl
188
+ )
189
+ logger.info(`[VWR Config] Trying shellVersion: ${url}`)
190
+ return tryGetVWRConfig(url, timeout)
191
+ } catch (error) {
192
+ logger.error(
193
+ `[VWR Config] URL construction failed for shellVersion config: ${error}`
194
+ )
195
+ return null
196
+ }
197
+ }
198
+
199
+ const tryGetVWRConfig = async (
200
+ url: URL,
201
+ timeout: number
202
+ ): Promise<VWRConfig | null> => {
203
+ const controller = new AbortController()
204
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
205
+
206
+ try {
207
+ const response = await fetch(
208
+ new Request(url, {
209
+ headers: {
210
+ "Content-Type": "application/json",
211
+ },
212
+ signal: controller.signal,
213
+ })
214
+ )
215
+
216
+ if (!response.ok) {
217
+ clearTimeout(timeoutId)
218
+ return null
219
+ }
220
+
221
+ let config = await response.json()
222
+ config = parseConfig(config)
223
+
224
+ clearTimeout(timeoutId)
225
+ return config as VWRConfig
226
+ } catch {
227
+ clearTimeout(timeoutId)
228
+ return null
229
+ }
230
+ }
231
+
232
+ const parseConfig = (config: VWRConfig): VWRConfig => {
233
+ const defaultConfig = getDefaultConfig()
234
+
235
+ if (
236
+ !Array.isArray(config.trustedDomains) ||
237
+ config.trustedDomains.length === 0
238
+ ) {
239
+ config.trustedDomains = defaultConfig.trustedDomains
240
+ }
241
+
242
+ if (!config.vwrUrl) {
243
+ config.vwrUrl = defaultConfig.vwrUrl
244
+ if (!config.trustedDomains.includes(defaultConfig.vwrUrl)) {
245
+ config.trustedDomains.push(defaultConfig.vwrUrl)
246
+ }
247
+ }
248
+
249
+ if (!config.hubUrl) {
250
+ config.hubUrl = defaultConfig.hubUrl
251
+ if (!config.trustedDomains.includes(defaultConfig.hubUrl)) {
252
+ config.trustedDomains.push(defaultConfig.hubUrl)
253
+ }
254
+ }
255
+
256
+ // launchUrl is optional - only add to trustedDomains if explicitly set
257
+ if (config.launchUrl && !config.trustedDomains.includes(config.launchUrl)) {
258
+ config.trustedDomains.push(config.launchUrl)
259
+ }
260
+
261
+ return config
262
+ }
263
+
264
+ export const validateConfig = (config: VWRConfig): boolean => {
265
+ if (!config.vwrUrl) return false
266
+
267
+ if (!config.hubUrl) return false
268
+
269
+ // launchUrl is optional, allow undefined
270
+
271
+ if (!Array.isArray(config.trustedDomains)) return false
272
+ else if (config.trustedDomains.length === 0) return false
273
+
274
+ return true
275
+ }
276
+
277
+ const getDefaultConfig = (): VWRConfig => {
278
+ const ENVIRONMENT = import.meta.env.VITE_ENVIRONMENT || "dev"
279
+ const defaults = ENV_DEFAULTS[ENVIRONMENT]
280
+
281
+ if (!defaults) {
282
+ throw new Error(
283
+ `[VWR Config] Unknown environment: ${ENVIRONMENT}. Valid: local, dev, staging, prod`
284
+ )
285
+ }
286
+
287
+ return {
288
+ hubUrl: defaults.hubUrl,
289
+ vwrUrl: defaults.vwrUrl,
290
+ launchUrl: undefined,
291
+ trustedDomains: [defaults.hubUrl, defaults.vwrUrl],
292
+ }
293
+ }