@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,172 @@
1
+ import { ENV_DEFAULTS } from "./envDefaults";
2
+ import { defaultLogger } from "./logger";
3
+ const DEFAULT_CONFIG_TIMEOUT = 2000;
4
+ /**
5
+ * Build a URL from a base and relative path, handling missing trailing slashes.
6
+ *
7
+ * The URL constructor resolves relative paths against the "directory" of the base URL,
8
+ * which is defined as everything up to (but not including) the last path segment.
9
+ * Without a trailing slash, the last segment is treated as a "file" and gets replaced.
10
+ *
11
+ * Example:
12
+ * new URL("a/b.json", "https://x.com/config") → "https://x.com/a/b.json" (wrong)
13
+ * new URL("a/b.json", "https://x.com/config/") → "https://x.com/config/a/b.json" (correct)
14
+ *
15
+ * This helper normalizes the base URL to ensure correct resolution.
16
+ */
17
+ const buildUrl = (path, base) => {
18
+ const normalizedBase = base.endsWith("/") ? base : `${base}/`;
19
+ return new URL(path, normalizedBase);
20
+ };
21
+ export const getVWRConfig = async (request, logger = defaultLogger) => {
22
+ var _a;
23
+ const timeout = (_a = request.timeout) !== null && _a !== void 0 ? _a : DEFAULT_CONFIG_TIMEOUT;
24
+ logger.info("[VWR Config] Fetching config with priority: local → device → shellVersion → environment → defaults");
25
+ // Fetch all configs in parallel for performance
26
+ const [localConfig, deviceConfig, shellVersionConfig, environmentConfig] = await Promise.all([
27
+ tryGetLocalConfig(request.configFile, timeout, logger),
28
+ tryGetDeviceConfig(request.configUrl, request.configFile, request.platform, request.deviceId, timeout, logger),
29
+ tryGetShellVersionConfig(request.configUrl, request.configFile, request.environment, request.platform, request.shellVersion, timeout, logger),
30
+ tryGetEnvironmentConfig(request.configUrl, request.configFile, request.environment, timeout, logger),
31
+ ]);
32
+ // Return first successful config in priority order
33
+ if (localConfig !== null) {
34
+ logger.info("[VWR Config] ✓ Using config from: local", localConfig);
35
+ return localConfig;
36
+ }
37
+ if (deviceConfig !== null) {
38
+ logger.info("[VWR Config] ✓ Using config from: device", deviceConfig);
39
+ return deviceConfig;
40
+ }
41
+ if (shellVersionConfig !== null) {
42
+ logger.info("[VWR Config] ✓ Using config from: shellVersion", shellVersionConfig);
43
+ return shellVersionConfig;
44
+ }
45
+ if (environmentConfig !== null) {
46
+ logger.info("[VWR Config] ✓ Using config from: environment", environmentConfig);
47
+ return environmentConfig;
48
+ }
49
+ // All fetches failed, use defaults
50
+ logger.warn("[VWR Config] All config fetches failed, using built-in defaults");
51
+ const defaultConfig = getDefaultConfig();
52
+ return defaultConfig;
53
+ };
54
+ const tryGetLocalConfig = async (configFile, timeout, logger) => {
55
+ var _a, _b;
56
+ try {
57
+ const url = buildUrl(configFile, (_b = (_a = ENV_DEFAULTS["local"]) === null || _a === void 0 ? void 0 : _a.configUrl) !== null && _b !== void 0 ? _b : "");
58
+ logger.info(`[VWR Config] Trying local: ${url}`);
59
+ return tryGetVWRConfig(url, timeout);
60
+ }
61
+ catch (error) {
62
+ logger.error(`[VWR Config] URL construction failed for local config: ${error}`);
63
+ return null;
64
+ }
65
+ };
66
+ const tryGetDeviceConfig = async (configUrl, configFile, platform, deviceId, timeout, logger) => {
67
+ try {
68
+ const url = buildUrl(`device/${platform}/${deviceId}/${configFile}`, configUrl);
69
+ logger.info(`[VWR Config] Trying device: ${url}`);
70
+ return tryGetVWRConfig(url, timeout);
71
+ }
72
+ catch (error) {
73
+ logger.error(`[VWR Config] URL construction failed for device config: ${error}`);
74
+ return null;
75
+ }
76
+ };
77
+ const tryGetEnvironmentConfig = async (configUrl, configFile, environment, timeout, logger) => {
78
+ try {
79
+ const url = buildUrl(`environments/${environment}/${configFile}`, configUrl);
80
+ logger.info(`[VWR Config] Trying environment: ${url}`);
81
+ return tryGetVWRConfig(url, timeout);
82
+ }
83
+ catch (error) {
84
+ logger.error(`[VWR Config] URL construction failed for environment config: ${error}`);
85
+ return null;
86
+ }
87
+ };
88
+ const tryGetShellVersionConfig = async (configUrl, configFile, environment, platform, shellVersion, timeout, logger) => {
89
+ try {
90
+ const url = buildUrl(`shellVersion/${environment}/${platform}/${shellVersion}/${configFile}`, configUrl);
91
+ logger.info(`[VWR Config] Trying shellVersion: ${url}`);
92
+ return tryGetVWRConfig(url, timeout);
93
+ }
94
+ catch (error) {
95
+ logger.error(`[VWR Config] URL construction failed for shellVersion config: ${error}`);
96
+ return null;
97
+ }
98
+ };
99
+ const tryGetVWRConfig = async (url, timeout) => {
100
+ const controller = new AbortController();
101
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
102
+ try {
103
+ const response = await fetch(new Request(url, {
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ },
107
+ signal: controller.signal,
108
+ }));
109
+ if (!response.ok) {
110
+ clearTimeout(timeoutId);
111
+ return null;
112
+ }
113
+ let config = await response.json();
114
+ config = parseConfig(config);
115
+ clearTimeout(timeoutId);
116
+ return config;
117
+ }
118
+ catch (_a) {
119
+ clearTimeout(timeoutId);
120
+ return null;
121
+ }
122
+ };
123
+ const parseConfig = (config) => {
124
+ const defaultConfig = getDefaultConfig();
125
+ if (!Array.isArray(config.trustedDomains) ||
126
+ config.trustedDomains.length === 0) {
127
+ config.trustedDomains = defaultConfig.trustedDomains;
128
+ }
129
+ if (!config.vwrUrl) {
130
+ config.vwrUrl = defaultConfig.vwrUrl;
131
+ if (!config.trustedDomains.includes(defaultConfig.vwrUrl)) {
132
+ config.trustedDomains.push(defaultConfig.vwrUrl);
133
+ }
134
+ }
135
+ if (!config.hubUrl) {
136
+ config.hubUrl = defaultConfig.hubUrl;
137
+ if (!config.trustedDomains.includes(defaultConfig.hubUrl)) {
138
+ config.trustedDomains.push(defaultConfig.hubUrl);
139
+ }
140
+ }
141
+ // launchUrl is optional - only add to trustedDomains if explicitly set
142
+ if (config.launchUrl && !config.trustedDomains.includes(config.launchUrl)) {
143
+ config.trustedDomains.push(config.launchUrl);
144
+ }
145
+ return config;
146
+ };
147
+ export const validateConfig = (config) => {
148
+ if (!config.vwrUrl)
149
+ return false;
150
+ if (!config.hubUrl)
151
+ return false;
152
+ // launchUrl is optional, allow undefined
153
+ if (!Array.isArray(config.trustedDomains))
154
+ return false;
155
+ else if (config.trustedDomains.length === 0)
156
+ return false;
157
+ return true;
158
+ };
159
+ const getDefaultConfig = () => {
160
+ const ENVIRONMENT = import.meta.env.VITE_ENVIRONMENT || "dev";
161
+ const defaults = ENV_DEFAULTS[ENVIRONMENT];
162
+ if (!defaults) {
163
+ throw new Error(`[VWR Config] Unknown environment: ${ENVIRONMENT}. Valid: local, dev, staging, prod`);
164
+ }
165
+ return {
166
+ hubUrl: defaults.hubUrl,
167
+ vwrUrl: defaults.vwrUrl,
168
+ launchUrl: undefined,
169
+ trustedDomains: [defaults.hubUrl, defaults.vwrUrl],
170
+ };
171
+ };
172
+ //# sourceMappingURL=vwrConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vwrConfig.js","sourceRoot":"","sources":["../src/vwrConfig.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@volley/vwr-loader",
3
+ "version": "1.0.0-alpha.1",
4
+ "type": "module",
5
+ "description": "Vite-based VWR loader for all Volley platforms",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./amplitudeFlagFetcher": "./dist/amplitudeFlagFetcher.js",
11
+ "./getDeviceId": "./dist/getDeviceId.js",
12
+ "./getShellVersion": "./dist/getShellVersion.js",
13
+ "./envDefaults": "./dist/envDefaults.js"
14
+ },
15
+ "bin": {
16
+ "build-loader": "./dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc && vite build && vite build -c vite.cli.config.ts && chmod +x dist/cli.js",
24
+ "build:cli": "vite build -c vite.cli.config.ts",
25
+ "build:loader": "vite build",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "format": "eslint . --fix",
30
+ "lint": "eslint .",
31
+ "semantic-release": "semantic-release"
32
+ },
33
+ "dependencies": {
34
+ "commander": "^12.0.0",
35
+ "dotenv": "^16.0.0",
36
+ "fs-extra": "^11.0.0",
37
+ "tsx": "^4.7.0",
38
+ "vite": "^5.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@platform/eslint-config": "workspace:*",
42
+ "@semantic-release/git": "^10.0.1",
43
+ "@semantic-release/npm": "^12.0.1",
44
+ "@types/fs-extra": "^11.0.0",
45
+ "@types/node": "^20.0.0",
46
+ "eslint": "^9.25.1",
47
+ "jsdom": "^24.1.3",
48
+ "prettier": "~3.0.3",
49
+ "semantic-release": "^24.2.3",
50
+ "semantic-release-monorepo": "^8.0.2",
51
+ "typescript": "^5.0.0",
52
+ "vitest": "^1.0.0"
53
+ }
54
+ }
@@ -0,0 +1,209 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ import { fetchAmplitudeFlags } from "./amplitudeFlagFetcher"
4
+ import * as getShellVersionModule from "./getShellVersion"
5
+
6
+ describe("fetchAmplitudeFlags", () => {
7
+ const mockFetch = vi.fn()
8
+ const originalLocation = window.location
9
+
10
+ beforeEach(() => {
11
+ global.fetch = mockFetch
12
+ vi.clearAllMocks()
13
+
14
+ // Mock window.location for getShellVersion
15
+ delete (window as any).location
16
+ window.location = {
17
+ ...originalLocation,
18
+ href: "http://localhost:3000/?version=1.0.0",
19
+ } as Location
20
+ })
21
+
22
+ afterEach(() => {
23
+ window.location = originalLocation
24
+ vi.restoreAllMocks()
25
+ })
26
+
27
+ it("fetches flags successfully from Amplitude with context", async () => {
28
+ // Mock getShellVersion to return expected version for SAMSUNG_TV
29
+ vi.spyOn(getShellVersionModule, "getShellVersion").mockResolvedValue(
30
+ "1.0.0"
31
+ )
32
+
33
+ mockFetch.mockResolvedValueOnce({
34
+ ok: true,
35
+ json: async () => ({
36
+ "vwr-enabled": {
37
+ payload: {
38
+ "vwr-enabled": true,
39
+ },
40
+ },
41
+ }),
42
+ })
43
+
44
+ const result = await fetchAmplitudeFlags("device-123", "SAMSUNG_TV", {
45
+ apiKey: "test-key",
46
+ })
47
+
48
+ expect(result).toEqual({
49
+ "vwr-enabled": true,
50
+ })
51
+
52
+ // Verify context is included in the request
53
+ const callArgs = mockFetch.mock.calls[0]
54
+ const url = new URL(callArgs[0])
55
+ const context = JSON.parse(url.searchParams.get("context") || "{}")
56
+
57
+ expect(context).toEqual({
58
+ user_properties: {
59
+ platformEnum: "SAMSUNG_TV",
60
+ nativeShellAppVersion: "1.0.0",
61
+ },
62
+ })
63
+
64
+ expect(mockFetch).toHaveBeenCalledWith(
65
+ expect.stringContaining("user_id=device-123"),
66
+ expect.objectContaining({
67
+ method: "GET",
68
+ headers: { Authorization: "Api-Key test-key" },
69
+ })
70
+ )
71
+ })
72
+
73
+ it("includes provided shell version in context", async () => {
74
+ mockFetch.mockResolvedValueOnce({
75
+ ok: true,
76
+ json: async () => ({
77
+ "vwr-enabled": {
78
+ payload: {
79
+ "vwr-enabled": true,
80
+ },
81
+ },
82
+ }),
83
+ })
84
+
85
+ await fetchAmplitudeFlags(
86
+ "device-123",
87
+ "SAMSUNG_TV",
88
+ {
89
+ apiKey: "test-key",
90
+ },
91
+ "2.5.0"
92
+ )
93
+
94
+ const callArgs = mockFetch.mock.calls[0]
95
+ const url = new URL(callArgs[0])
96
+ const context = JSON.parse(url.searchParams.get("context") || "{}")
97
+
98
+ expect(context).toEqual({
99
+ user_properties: {
100
+ platformEnum: "SAMSUNG_TV",
101
+ nativeShellAppVersion: "2.5.0",
102
+ },
103
+ })
104
+ })
105
+
106
+ it("auto-fetches shell version when not provided", async () => {
107
+ const getShellVersionSpy = vi.spyOn(
108
+ getShellVersionModule,
109
+ "getShellVersion"
110
+ )
111
+
112
+ mockFetch.mockResolvedValueOnce({
113
+ ok: true,
114
+ json: async () => ({
115
+ "vwr-enabled": {
116
+ payload: {
117
+ "vwr-enabled": false,
118
+ },
119
+ },
120
+ }),
121
+ })
122
+
123
+ await fetchAmplitudeFlags("device-456", "LG_TV", {
124
+ apiKey: "test-key",
125
+ })
126
+
127
+ expect(getShellVersionSpy).toHaveBeenCalledWith("LG_TV")
128
+
129
+ const callArgs = mockFetch.mock.calls[0]
130
+ const url = new URL(callArgs[0])
131
+ const context = JSON.parse(url.searchParams.get("context") || "{}")
132
+
133
+ expect(context.user_properties).toHaveProperty("nativeShellAppVersion")
134
+ })
135
+
136
+ it("returns safe defaults on timeout", async () => {
137
+ mockFetch.mockImplementationOnce(
138
+ () => new Promise((resolve) => setTimeout(resolve, 3000))
139
+ )
140
+
141
+ const result = await fetchAmplitudeFlags("device-123", "FIRE_TV", {
142
+ apiKey: "test-key",
143
+ timeout: 100,
144
+ })
145
+
146
+ expect(result).toEqual({
147
+ "vwr-enabled": false,
148
+ })
149
+ })
150
+
151
+ it("returns safe defaults on API error", async () => {
152
+ mockFetch.mockResolvedValueOnce({
153
+ ok: false,
154
+ status: 500,
155
+ })
156
+
157
+ const result = await fetchAmplitudeFlags("device-123", "MOBILE", {
158
+ apiKey: "test-key",
159
+ })
160
+
161
+ expect(result).toEqual({
162
+ "vwr-enabled": false,
163
+ })
164
+ })
165
+
166
+ it("returns safe defaults on network error", async () => {
167
+ mockFetch.mockRejectedValueOnce(new Error("Network error"))
168
+
169
+ const result = await fetchAmplitudeFlags("device-123", "MOBILE", {
170
+ apiKey: "test-key",
171
+ })
172
+
173
+ expect(result).toEqual({
174
+ "vwr-enabled": false,
175
+ })
176
+ })
177
+
178
+ it("handles missing flag values in response", async () => {
179
+ mockFetch.mockResolvedValueOnce({
180
+ ok: true,
181
+ json: async () => ({}),
182
+ })
183
+
184
+ const result = await fetchAmplitudeFlags("device-123", "LG_TV", {
185
+ apiKey: "test-key",
186
+ })
187
+
188
+ expect(result).toEqual({
189
+ "vwr-enabled": false,
190
+ })
191
+ })
192
+
193
+ it("handles partial response with missing payload", async () => {
194
+ mockFetch.mockResolvedValueOnce({
195
+ ok: true,
196
+ json: async () => ({
197
+ "vwr-enabled": {},
198
+ }),
199
+ })
200
+
201
+ const result = await fetchAmplitudeFlags("device-123", "LG_TV", {
202
+ apiKey: "test-key",
203
+ })
204
+
205
+ expect(result).toEqual({
206
+ "vwr-enabled": false,
207
+ })
208
+ })
209
+ })
@@ -0,0 +1,88 @@
1
+ import { getShellVersion } from "./getShellVersion"
2
+
3
+ export interface AmplitudeConfig {
4
+ apiKey: string
5
+ apiUrl?: string
6
+ timeout?: number
7
+ }
8
+
9
+ export interface FlagResult {
10
+ "vwr-enabled": boolean
11
+ }
12
+
13
+ /**
14
+ * Fetch feature flags from Amplitude Experiment REST API.
15
+ *
16
+ * Uses GET method with query parameters and Authorization header.
17
+ * Includes platform and shell version in context for better flag targeting.
18
+ * Compatible with Chrome 66+ (Fetch API, async/await, AbortController).
19
+ *
20
+ * @param deviceId - Unique device identifier
21
+ * @param platform - Platform name (samsung, lg, firetv, ios, android)
22
+ * @param config - Amplitude configuration
23
+ * @param shellVersion - Optional shell version (if not provided, will be fetched automatically)
24
+ * @returns Flag values with safe defaults on error
25
+ */
26
+ export async function fetchAmplitudeFlags(
27
+ deviceId: string,
28
+ platform: string,
29
+ config: AmplitudeConfig,
30
+ shellVersion?: string
31
+ ): Promise<FlagResult> {
32
+ const {
33
+ apiKey,
34
+ apiUrl = "https://api.lab.amplitude.com/v1/vardata",
35
+ timeout = 2000,
36
+ } = config
37
+
38
+ const controller = new AbortController()
39
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
40
+
41
+ try {
42
+ // Get shell version if not provided
43
+ const version = shellVersion ?? (await getShellVersion(platform))
44
+
45
+ // Build context with user_properties
46
+ const context = {
47
+ user_properties: {
48
+ platformEnum: platform,
49
+ nativeShellAppVersion: version,
50
+ },
51
+ }
52
+
53
+ // Build query parameters
54
+ const params = new URLSearchParams({
55
+ user_id: deviceId,
56
+ flag_keys: "vwr-enabled",
57
+ context: JSON.stringify(context),
58
+ })
59
+
60
+ const response = await fetch(`${apiUrl}?${params}`, {
61
+ method: "GET",
62
+ headers: {
63
+ Authorization: `Api-Key ${apiKey}`,
64
+ },
65
+ signal: controller.signal,
66
+ })
67
+
68
+ clearTimeout(timeoutId)
69
+
70
+ if (!response.ok) {
71
+ throw new Error(`Amplitude API returned ${response.status}`)
72
+ }
73
+
74
+ const data = await response.json()
75
+
76
+ return {
77
+ "vwr-enabled":
78
+ data["vwr-enabled"]?.payload?.["vwr-enabled"] ?? false,
79
+ }
80
+ } catch (error) {
81
+ console.error("[Shell] Amplitude flag fetch failed:", error)
82
+
83
+ // Return safe defaults - runs legacy path
84
+ return {
85
+ "vwr-enabled": false,
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,45 @@
1
+ export interface EnvConfig {
2
+ hubUrl: string
3
+ vwrUrl: string
4
+ configUrl: string
5
+ configFile: string
6
+ amplitudeKey: string // Used for amplitude flag fetch, not part of VWRConfig
7
+ }
8
+
9
+ const CONFIG_URL_DEFAULT = "https://vwr.volley.tv/config/"
10
+ const CONFIG_FILE_DEFAULT = "vwrConfig.json"
11
+ const VWR_URL_PATH_DEFAULT = "v1/latest/vwr.js"
12
+
13
+ const prodConfig: EnvConfig = {
14
+ hubUrl: "https://game-clients.volley.tv/hub",
15
+ vwrUrl: `https://vwr.volley.tv/${VWR_URL_PATH_DEFAULT}`,
16
+ configUrl: CONFIG_URL_DEFAULT,
17
+ configFile: CONFIG_FILE_DEFAULT,
18
+ amplitudeKey: "",
19
+ }
20
+
21
+ export const ENV_DEFAULTS: Record<string, EnvConfig> = {
22
+ local: {
23
+ hubUrl: "http://localhost:5173",
24
+ vwrUrl: "http://localhost:5174/vwr.js",
25
+ configUrl: "http://localhost:5174/config/",
26
+ configFile: CONFIG_FILE_DEFAULT,
27
+ amplitudeKey: "client-uJJVW3zKPC1G9kqPhUumLnZN6eaY42iQ",
28
+ },
29
+ dev: {
30
+ hubUrl: "https://game-clients-dev.volley.tv/hub",
31
+ vwrUrl: `https://vwr.volley.tv/dev/${VWR_URL_PATH_DEFAULT}`,
32
+ configUrl: CONFIG_URL_DEFAULT,
33
+ configFile: CONFIG_FILE_DEFAULT,
34
+ amplitudeKey: "client-uJJVW3zKPC1G9kqPhUumLnZN6eaY42iQ",
35
+ },
36
+ staging: {
37
+ hubUrl: "https://game-clients-staging.volley.tv/hub",
38
+ vwrUrl: `https://vwr.volley.tv/staging/${VWR_URL_PATH_DEFAULT}`,
39
+ configUrl: CONFIG_URL_DEFAULT,
40
+ configFile: CONFIG_FILE_DEFAULT,
41
+ amplitudeKey: "",
42
+ },
43
+ prod: prodConfig,
44
+ production: prodConfig, // Alias for prod
45
+ }