@omen.foundation/node-microservice-runtime 0.1.100 → 0.1.102

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.
@@ -69,31 +69,70 @@ function loadDeveloperEnvVars(beamEnvPath) {
69
69
  }
70
70
  return envVars;
71
71
  }
72
- async function fetchBeamableConfig(env) {
72
+ async function fetchBeamableConfig(env, timeoutMs = 2000) {
73
73
  const configVars = {};
74
- if (!env.secret || !env.cid || !env.pid) {
75
- console.log('[EnvLoader] Skipping Beamable Config fetch - missing SECRET, CID, or PID');
74
+ if (!env.cid || !env.pid) {
75
+ console.log('[EnvLoader] Skipping Beamable Config fetch - missing CID or PID');
76
+ return configVars;
77
+ }
78
+ const accessToken = process.env.ACCESS_TOKEN || process.env.BEAMABLE_TOKEN;
79
+ const hasSecret = !!env.secret;
80
+ if (!accessToken && !hasSecret) {
81
+ console.log('[EnvLoader] Skipping Beamable Config fetch - missing ACCESS_TOKEN/BEAMABLE_TOKEN or SECRET');
76
82
  return configVars;
77
83
  }
78
84
  try {
79
85
  const apiUrl = (0, urls_js_1.hostToHttpUrl)(env.host);
80
- const uriPath = process.env.BEAM_CONFIG_API_PATH || '/api/basic/realms/config';
86
+ const uriPath = process.env.BEAM_CONFIG_API_PATH || '/basic/realms/config';
81
87
  const configUrl = new node_url_1.URL(uriPath, apiUrl).toString();
82
- const pathAndQuery = uriPath;
83
- const signature = calculateSignature(env.pid, env.secret, pathAndQuery, null, '1');
88
+ const headers = {
89
+ 'Content-Type': 'application/json',
90
+ Accept: 'application/json',
91
+ 'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,
92
+ };
93
+ if (accessToken) {
94
+ headers['Authorization'] = `Bearer ${accessToken}`;
95
+ }
96
+ else if (hasSecret && env.secret) {
97
+ const pathAndQuery = uriPath;
98
+ const signature = calculateSignature(env.pid, env.secret, pathAndQuery, null, '1');
99
+ headers['X-BEAM-SIGNATURE'] = signature;
100
+ }
84
101
  console.log(`[EnvLoader] Fetching Beamable Config from ${configUrl}...`);
85
- const response = await fetch(configUrl, {
86
- method: 'GET',
87
- headers: {
88
- 'Content-Type': 'application/json',
89
- Accept: 'application/json',
90
- 'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,
91
- 'X-BEAM-SIGNATURE': signature,
92
- },
93
- });
102
+ console.log(`[EnvLoader] Using ${accessToken ? 'token-based' : 'signature-based'} authentication`);
103
+ const controller = new AbortController();
104
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
105
+ let response;
106
+ try {
107
+ response = await fetch(configUrl, {
108
+ method: 'GET',
109
+ headers,
110
+ signal: controller.signal,
111
+ });
112
+ clearTimeout(timeoutId);
113
+ }
114
+ catch (error) {
115
+ clearTimeout(timeoutId);
116
+ if (error instanceof Error && error.name === 'AbortError') {
117
+ console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms`);
118
+ console.warn(`[EnvLoader] URL: ${configUrl}`);
119
+ console.warn(`[EnvLoader] Auth method: ${accessToken ? 'token-based' : 'signature-based'}`);
120
+ throw new Error(`Beamable Config fetch timed out after ${timeoutMs}ms`);
121
+ }
122
+ throw error;
123
+ }
94
124
  if (!response.ok) {
95
125
  const errorText = await response.text().catch(() => 'Unknown error');
96
- console.warn(`[EnvLoader] Failed to fetch Beamable Config: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
126
+ console.warn(`[EnvLoader] Failed to fetch Beamable Config: ${response.status} ${response.statusText}`);
127
+ console.warn(`[EnvLoader] URL: ${configUrl}`);
128
+ console.warn(`[EnvLoader] Auth method: ${accessToken ? 'token-based' : 'signature-based'}`);
129
+ if (response.status === 401 || response.status === 403) {
130
+ console.warn(`[EnvLoader] Authentication failed. This endpoint requires token-based auth (ACCESS_TOKEN or BEAMABLE_TOKEN).`);
131
+ if (!accessToken) {
132
+ console.warn(`[EnvLoader] Tip: Set ACCESS_TOKEN or BEAMABLE_TOKEN in your environment to enable Beamable Config fetching.`);
133
+ }
134
+ }
135
+ console.warn(`[EnvLoader] Response: ${errorText.substring(0, 500)}`);
97
136
  return configVars;
98
137
  }
99
138
  const data = (await response.json());
@@ -142,7 +181,7 @@ async function loadAndInjectEnvironmentVariables(env, beamEnvPath, waitForConfig
142
181
  }
143
182
  if (waitForConfig) {
144
183
  try {
145
- const configPromise = fetchBeamableConfig(env);
184
+ const configPromise = fetchBeamableConfig(env, timeoutMs);
146
185
  const timeoutPromise = new Promise((resolve) => {
147
186
  setTimeout(() => {
148
187
  console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms, continuing without it`);
@@ -1 +1 @@
1
- {"version":3,"file":"env-loader.d.ts","sourceRoot":"","sources":["../src/env-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAsBpD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAenE;AA2KD;;;;;;;;;;;;GAYG;AACH,wBAAsB,iCAAiC,CACrD,GAAG,EAAE,iBAAiB,EACtB,WAAW,CAAC,EAAE,MAAM,EACpB,aAAa,GAAE,OAAc,EAC7B,SAAS,GAAE,MAAa,GACvB,OAAO,CAAC,IAAI,CAAC,CAoEf"}
1
+ {"version":3,"file":"env-loader.d.ts","sourceRoot":"","sources":["../src/env-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAsBpD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAenE;AA0ND;;;;;;;;;;;;GAYG;AACH,wBAAsB,iCAAiC,CACrD,GAAG,EAAE,iBAAiB,EACtB,WAAW,CAAC,EAAE,MAAM,EACpB,aAAa,GAAE,OAAc,EAC7B,SAAS,GAAE,MAAa,GACvB,OAAO,CAAC,IAAI,CAAC,CAoEf"}
@@ -110,34 +110,78 @@ function loadDeveloperEnvVars(beamEnvPath) {
110
110
  *
111
111
  * Returns key-value pairs from the current realm's config namespace/environment
112
112
  */
113
- async function fetchBeamableConfig(env) {
113
+ async function fetchBeamableConfig(env, timeoutMs = 2000) {
114
114
  const configVars = {};
115
115
  // Skip if we don't have required credentials
116
- if (!env.secret || !env.cid || !env.pid) {
117
- console.log('[EnvLoader] Skipping Beamable Config fetch - missing SECRET, CID, or PID');
116
+ if (!env.cid || !env.pid) {
117
+ console.log('[EnvLoader] Skipping Beamable Config fetch - missing CID or PID');
118
+ return configVars;
119
+ }
120
+ // Check if we have an access token (preferred) or secret (fallback)
121
+ const accessToken = process.env.ACCESS_TOKEN || process.env.BEAMABLE_TOKEN;
122
+ const hasSecret = !!env.secret;
123
+ if (!accessToken && !hasSecret) {
124
+ console.log('[EnvLoader] Skipping Beamable Config fetch - missing ACCESS_TOKEN/BEAMABLE_TOKEN or SECRET');
118
125
  return configVars;
119
126
  }
120
127
  try {
121
128
  const apiUrl = hostToHttpUrl(env.host);
122
129
  // Allow endpoint to be configured via environment variable (for flexibility)
123
- const uriPath = process.env.BEAM_CONFIG_API_PATH || '/api/basic/realms/config';
130
+ // Default endpoint matches C# SDK and Portal: /basic/realms/config (not /api/basic/realms/config)
131
+ const uriPath = process.env.BEAM_CONFIG_API_PATH || '/basic/realms/config';
124
132
  const configUrl = new URL(uriPath, apiUrl).toString();
125
- // Calculate signature for signed request
126
- const pathAndQuery = uriPath;
127
- const signature = calculateSignature(env.pid, env.secret, pathAndQuery, null, '1');
133
+ // Build headers - prefer token-based auth if available, fallback to signature-based
134
+ const headers = {
135
+ 'Content-Type': 'application/json',
136
+ Accept: 'application/json',
137
+ 'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,
138
+ };
139
+ if (accessToken) {
140
+ // Use token-based authentication (matches Portal's $beamable.get())
141
+ headers['Authorization'] = `Bearer ${accessToken}`;
142
+ }
143
+ else if (hasSecret && env.secret) {
144
+ // Fallback to signature-based authentication
145
+ const pathAndQuery = uriPath;
146
+ const signature = calculateSignature(env.pid, env.secret, pathAndQuery, null, '1');
147
+ headers['X-BEAM-SIGNATURE'] = signature;
148
+ }
128
149
  console.log(`[EnvLoader] Fetching Beamable Config from ${configUrl}...`);
129
- const response = await fetch(configUrl, {
130
- method: 'GET',
131
- headers: {
132
- 'Content-Type': 'application/json',
133
- Accept: 'application/json',
134
- 'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,
135
- 'X-BEAM-SIGNATURE': signature,
136
- },
137
- });
150
+ console.log(`[EnvLoader] Using ${accessToken ? 'token-based' : 'signature-based'} authentication`);
151
+ // Add timeout to fetch to prevent hanging requests
152
+ const controller = new AbortController();
153
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
154
+ let response;
155
+ try {
156
+ response = await fetch(configUrl, {
157
+ method: 'GET',
158
+ headers,
159
+ signal: controller.signal,
160
+ });
161
+ clearTimeout(timeoutId);
162
+ }
163
+ catch (error) {
164
+ clearTimeout(timeoutId);
165
+ if (error instanceof Error && error.name === 'AbortError') {
166
+ console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms`);
167
+ console.warn(`[EnvLoader] URL: ${configUrl}`);
168
+ console.warn(`[EnvLoader] Auth method: ${accessToken ? 'token-based' : 'signature-based'}`);
169
+ throw new Error(`Beamable Config fetch timed out after ${timeoutMs}ms`);
170
+ }
171
+ throw error;
172
+ }
138
173
  if (!response.ok) {
139
174
  const errorText = await response.text().catch(() => 'Unknown error');
140
- console.warn(`[EnvLoader] Failed to fetch Beamable Config: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
175
+ console.warn(`[EnvLoader] Failed to fetch Beamable Config: ${response.status} ${response.statusText}`);
176
+ console.warn(`[EnvLoader] URL: ${configUrl}`);
177
+ console.warn(`[EnvLoader] Auth method: ${accessToken ? 'token-based' : 'signature-based'}`);
178
+ if (response.status === 401 || response.status === 403) {
179
+ console.warn(`[EnvLoader] Authentication failed. This endpoint requires token-based auth (ACCESS_TOKEN or BEAMABLE_TOKEN).`);
180
+ if (!accessToken) {
181
+ console.warn(`[EnvLoader] Tip: Set ACCESS_TOKEN or BEAMABLE_TOKEN in your environment to enable Beamable Config fetching.`);
182
+ }
183
+ }
184
+ console.warn(`[EnvLoader] Response: ${errorText.substring(0, 500)}`);
141
185
  return configVars;
142
186
  }
143
187
  const data = (await response.json());
@@ -208,7 +252,7 @@ export async function loadAndInjectEnvironmentVariables(env, beamEnvPath, waitFo
208
252
  // Fetch Beamable Config values (async, with optional timeout)
209
253
  if (waitForConfig) {
210
254
  try {
211
- const configPromise = fetchBeamableConfig(env);
255
+ const configPromise = fetchBeamableConfig(env, timeoutMs);
212
256
  const timeoutPromise = new Promise((resolve) => {
213
257
  setTimeout(() => {
214
258
  console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms, continuing without it`);
@@ -1 +1 @@
1
- {"version":3,"file":"env-loader.js","sourceRoot":"","sources":["../src/env-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * Environment Variable Loader\r\n * \r\n * This module handles loading environment variables from two sources:\r\n * 1. Developer-defined variables (from beam.env file)\r\n * 2. Beamable Config values (from API: namespace -> environment -> key-value pairs)\r\n * \r\n * All variables are injected into process.env before service initialization.\r\n */\r\n\r\nimport { existsSync, readFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport { createHash } from 'node:crypto';\r\nimport { URL } from 'node:url';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport { hostToHttpUrl } from './utils/urls.js';\r\n\r\n/**\r\n * Calculates Beamable signature for signed requests\r\n * Signature format: MD5(secret + pid + version + uriPathAndQuery + body) as Base64\r\n */\r\nfunction calculateSignature(\r\n pid: string,\r\n secret: string,\r\n uriPathAndQuery: string,\r\n body: string | null = null,\r\n version: string = '1'\r\n): string {\r\n let dataToSign = `${secret}${pid}${version}${uriPathAndQuery}`;\r\n if (body) {\r\n dataToSign += body;\r\n }\r\n const hash = createHash('md5').update(dataToSign, 'utf8').digest('base64');\r\n return hash;\r\n}\r\n\r\n/**\r\n * Loads developer-defined environment variables from beam.env file\r\n * Supports standard .env format: KEY=value\r\n * \r\n * This is exported for synchronous loading before logger initialization\r\n */\r\nexport function loadDeveloperEnvVarsSync(beamEnvPath?: string): void {\r\n const envVars = loadDeveloperEnvVars(beamEnvPath);\r\n \r\n // Inject immediately into process.env\r\n let injectedCount = 0;\r\n for (const [key, value] of Object.entries(envVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n injectedCount++;\r\n }\r\n }\r\n \r\n if (injectedCount > 0) {\r\n console.log(`[EnvLoader] Synchronously injected ${injectedCount} developer-defined variables from beam.env`);\r\n }\r\n}\r\n\r\n/**\r\n * Loads developer-defined environment variables from beam.env file\r\n * Supports standard .env format: KEY=value\r\n */\r\nfunction loadDeveloperEnvVars(beamEnvPath?: string): Record<string, string> {\r\n const envVars: Record<string, string> = {};\r\n \r\n // Try to find beam.env file\r\n const possiblePaths = [\r\n beamEnvPath,\r\n join(process.cwd(), 'beam.env'),\r\n join(process.cwd(), '.beam.env'),\r\n '/beam/service/beam.env', // Container path\r\n '/beam/service/.beam.env',\r\n ].filter((path): path is string => !!path && existsSync(path));\r\n\r\n if (possiblePaths.length === 0) {\r\n return envVars;\r\n }\r\n\r\n const envFilePath = possiblePaths[0];\r\n try {\r\n const content = readFileSync(envFilePath, 'utf-8');\r\n const lines = content.split('\\n');\r\n \r\n for (const line of lines) {\r\n const trimmed = line.trim();\r\n // Skip empty lines and comments\r\n if (!trimmed || trimmed.startsWith('#')) {\r\n continue;\r\n }\r\n \r\n // Parse KEY=value format\r\n const match = trimmed.match(/^([^=#]+)=(.*)$/);\r\n if (match) {\r\n const key = match[1].trim();\r\n let value = match[2].trim();\r\n \r\n // Remove quotes if present\r\n if ((value.startsWith('\"') && value.endsWith('\"')) || \r\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\r\n value = value.slice(1, -1);\r\n }\r\n \r\n if (key) {\r\n envVars[key] = value;\r\n }\r\n }\r\n }\r\n \r\n console.log(`[EnvLoader] Loaded ${Object.keys(envVars).length} variables from ${envFilePath}`);\r\n } catch (error) {\r\n console.warn(`[EnvLoader] Failed to load beam.env from ${envFilePath}:`, error instanceof Error ? error.message : String(error));\r\n }\r\n \r\n return envVars;\r\n}\r\n\r\n/**\r\n * Beamable Config API response structure\r\n */\r\ninterface BeamableConfigResponse {\r\n config?: {\r\n [key: string]: {\r\n [environment: string]: string | number | boolean | null;\r\n };\r\n };\r\n [key: string]: unknown;\r\n}\r\n\r\n/**\r\n * Fetches Beamable Config values from the API\r\n * \r\n * The endpoint can be configured via BEAM_CONFIG_API_PATH environment variable.\r\n * Default: /api/basic/realms/config\r\n * \r\n * Note: The exact endpoint format may vary. The API should return a structure like:\r\n * {\r\n * \"config\": {\r\n * \"key1\": {\r\n * \"environment\": \"value1\"\r\n * }\r\n * }\r\n * }\r\n * \r\n * Returns key-value pairs from the current realm's config namespace/environment\r\n */\r\nasync function fetchBeamableConfig(\r\n env: EnvironmentConfig\r\n): Promise<Record<string, string>> {\r\n const configVars: Record<string, string> = {};\r\n \r\n // Skip if we don't have required credentials\r\n if (!env.secret || !env.cid || !env.pid) {\r\n console.log('[EnvLoader] Skipping Beamable Config fetch - missing SECRET, CID, or PID');\r\n return configVars;\r\n }\r\n\r\n try {\r\n const apiUrl = hostToHttpUrl(env.host);\r\n // Allow endpoint to be configured via environment variable (for flexibility)\r\n const uriPath = process.env.BEAM_CONFIG_API_PATH || '/api/basic/realms/config';\r\n const configUrl = new URL(uriPath, apiUrl).toString();\r\n \r\n // Calculate signature for signed request\r\n const pathAndQuery = uriPath;\r\n const signature = calculateSignature(env.pid, env.secret, pathAndQuery, null, '1');\r\n \r\n console.log(`[EnvLoader] Fetching Beamable Config from ${configUrl}...`);\r\n \r\n const response = await fetch(configUrl, {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Accept: 'application/json',\r\n 'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,\r\n 'X-BEAM-SIGNATURE': signature,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n const errorText = await response.text().catch(() => 'Unknown error');\r\n console.warn(`[EnvLoader] Failed to fetch Beamable Config: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);\r\n return configVars;\r\n }\r\n\r\n const data = (await response.json()) as BeamableConfigResponse;\r\n \r\n // Parse config structure: config[key][environment] = value\r\n // We need to determine the current environment - typically it's the PID or a specific env var\r\n const currentEnvironment = process.env.BEAM_ENVIRONMENT || process.env.ENVIRONMENT || env.pid;\r\n \r\n if (data.config) {\r\n for (const [key, environments] of Object.entries(data.config)) {\r\n // Try to get value for current environment, fallback to first available\r\n let value: string | number | boolean | null | undefined;\r\n \r\n if (environments && typeof environments === 'object') {\r\n value = environments[currentEnvironment];\r\n \r\n // If no value for current environment, try common environment names\r\n if (value === undefined) {\r\n value = environments['production'] || \r\n environments['staging'] || \r\n environments['development'] ||\r\n environments['dev'] ||\r\n Object.values(environments)[0];\r\n }\r\n }\r\n \r\n // Convert value to string and add to env vars\r\n if (value !== undefined && value !== null) {\r\n // Use BEAM_CONFIG_ prefix to avoid conflicts\r\n const envKey = `BEAM_CONFIG_${key.toUpperCase()}`;\r\n configVars[envKey] = String(value);\r\n }\r\n }\r\n \r\n console.log(`[EnvLoader] Loaded ${Object.keys(configVars).length} variables from Beamable Config`);\r\n } else {\r\n console.log('[EnvLoader] Beamable Config response contains no config object');\r\n }\r\n } catch (error) {\r\n console.warn('[EnvLoader] Failed to fetch Beamable Config:', error instanceof Error ? error.message : String(error));\r\n }\r\n \r\n return configVars;\r\n}\r\n\r\n/**\r\n * Loads all environment variables from both sources and injects them into process.env\r\n * \r\n * Priority (highest to lowest):\r\n * 1. Existing process.env values (never overwrite)\r\n * 2. Developer-defined variables (beam.env)\r\n * 3. Beamable Config values (API)\r\n * \r\n * @param env - Environment configuration (CID, PID, HOST, SECRET)\r\n * @param beamEnvPath - Optional path to beam.env file\r\n * @param waitForConfig - If true, waits up to timeoutMs for Beamable Config. If false, returns immediately after developer vars.\r\n * @param timeoutMs - Maximum time to wait for Beamable Config (default: 2000ms)\r\n */\r\nexport async function loadAndInjectEnvironmentVariables(\r\n env: EnvironmentConfig,\r\n beamEnvPath?: string,\r\n waitForConfig: boolean = true,\r\n timeoutMs: number = 2000\r\n): Promise<void> {\r\n console.log('[EnvLoader] Starting environment variable loading...');\r\n \r\n // Load developer-defined variables from beam.env (synchronous, immediate)\r\n const developerVars = loadDeveloperEnvVars(beamEnvPath);\r\n \r\n // Inject developer vars immediately\r\n let injectedCount = 0;\r\n for (const [key, value] of Object.entries(developerVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n injectedCount++;\r\n }\r\n }\r\n \r\n if (injectedCount > 0) {\r\n console.log(`[EnvLoader] Injected ${injectedCount} developer-defined variables from beam.env`);\r\n }\r\n \r\n // Fetch Beamable Config values (async, with optional timeout)\r\n if (waitForConfig) {\r\n try {\r\n const configPromise = fetchBeamableConfig(env);\r\n const timeoutPromise = new Promise<Record<string, string>>((resolve) => {\r\n setTimeout(() => {\r\n console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms, continuing without it`);\r\n resolve({});\r\n }, timeoutMs);\r\n });\r\n \r\n const beamableConfigVars = await Promise.race([configPromise, timeoutPromise]);\r\n \r\n // Inject Beamable Config vars\r\n let configInjectedCount = 0;\r\n for (const [key, value] of Object.entries(beamableConfigVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n configInjectedCount++;\r\n }\r\n }\r\n \r\n if (configInjectedCount > 0) {\r\n console.log(`[EnvLoader] Injected ${configInjectedCount} variables from Beamable Config`);\r\n }\r\n } catch (error) {\r\n console.warn('[EnvLoader] Error loading Beamable Config (continuing anyway):', error instanceof Error ? error.message : String(error));\r\n }\r\n } else {\r\n // Start fetch in background (non-blocking)\r\n fetchBeamableConfig(env)\r\n .then((beamableConfigVars) => {\r\n let configInjectedCount = 0;\r\n for (const [key, value] of Object.entries(beamableConfigVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n configInjectedCount++;\r\n }\r\n }\r\n if (configInjectedCount > 0) {\r\n console.log(`[EnvLoader] Injected ${configInjectedCount} variables from Beamable Config (loaded asynchronously)`);\r\n }\r\n })\r\n .catch((error) => {\r\n console.warn('[EnvLoader] Failed to load Beamable Config in background:', error instanceof Error ? error.message : String(error));\r\n });\r\n }\r\n \r\n console.log('[EnvLoader] Environment variable loading completed');\r\n}\r\n\r\n"]}
1
+ {"version":3,"file":"env-loader.js","sourceRoot":"","sources":["../src/env-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;GAGG;AACH,SAAS,kBAAkB,CACzB,GAAW,EACX,MAAc,EACd,eAAuB,EACvB,OAAsB,IAAI,EAC1B,UAAkB,GAAG;IAErB,IAAI,UAAU,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,OAAO,GAAG,eAAe,EAAE,CAAC;IAC/D,IAAI,IAAI,EAAE,CAAC;QACT,UAAU,IAAI,IAAI,CAAC;IACrB,CAAC;IACD,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3E,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,WAAoB;IAC3D,MAAM,OAAO,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;IAElD,sCAAsC;IACtC,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACzB,aAAa,EAAE,CAAC;QAClB,CAAC;IACH,CAAC;IAED,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,sCAAsC,aAAa,4CAA4C,CAAC,CAAC;IAC/G,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,oBAAoB,CAAC,WAAoB;IAChD,MAAM,OAAO,GAA2B,EAAE,CAAC;IAE3C,4BAA4B;IAC5B,MAAM,aAAa,GAAG;QACpB,WAAW;QACX,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC;QAChC,wBAAwB,EAAE,iBAAiB;QAC3C,yBAAyB;KAC1B,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAE/D,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,gCAAgC;YAChC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxC,SAAS;YACX,CAAC;YAED,yBAAyB;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;YAC/C,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC5B,IAAI,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAE5B,2BAA2B;gBAC3B,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;oBAC9C,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBACnD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC7B,CAAC;gBAED,IAAI,GAAG,EAAE,CAAC;oBACR,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBACvB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,mBAAmB,WAAW,EAAE,CAAC,CAAC;IACjG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,4CAA4C,WAAW,GAAG,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnI,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAcD;;;;;;;;;;;;;;;;GAgBG;AACH,KAAK,UAAU,mBAAmB,CAChC,GAAsB,EACtB,YAAoB,IAAI;IAExB,MAAM,UAAU,GAA2B,EAAE,CAAC;IAE9C,6CAA6C;IAC7C,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,iEAAiE,CAAC,CAAC;QAC/E,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,oEAAoE;IACpE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC3E,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;IAE/B,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,4FAA4F,CAAC,CAAC;QAC1G,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,6EAA6E;QAC7E,kGAAkG;QAClG,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,sBAAsB,CAAC;QAC3E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QAEtD,oFAAoF;QACpF,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE;SACxC,CAAC;QAEF,IAAI,WAAW,EAAE,CAAC;YAChB,oEAAoE;YACpE,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,WAAW,EAAE,CAAC;QACrD,CAAC;aAAM,IAAI,SAAS,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACnC,6CAA6C;YAC7C,MAAM,YAAY,GAAG,OAAO,CAAC;YAC7B,MAAM,SAAS,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;YACnF,OAAO,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;QAC1C,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,6CAA6C,SAAS,KAAK,CAAC,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,qBAAqB,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,iBAAiB,iBAAiB,CAAC,CAAC;QAEnG,mDAAmD;QACnD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QAElE,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBAChC,MAAM,EAAE,KAAK;gBACb,OAAO;gBACP,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,SAAS,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC1D,OAAO,CAAC,IAAI,CAAC,qDAAqD,SAAS,IAAI,CAAC,CAAC;gBACjF,OAAO,CAAC,IAAI,CAAC,oBAAoB,SAAS,EAAE,CAAC,CAAC;gBAC9C,OAAO,CAAC,IAAI,CAAC,4BAA4B,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC;gBAC5F,MAAM,IAAI,KAAK,CAAC,yCAAyC,SAAS,IAAI,CAAC,CAAC;YAC1E,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC;YACrE,OAAO,CAAC,IAAI,CAAC,gDAAgD,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;YACvG,OAAO,CAAC,IAAI,CAAC,oBAAoB,SAAS,EAAE,CAAC,CAAC;YAC9C,OAAO,CAAC,IAAI,CAAC,4BAA4B,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC;YAC5F,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACvD,OAAO,CAAC,IAAI,CAAC,8GAA8G,CAAC,CAAC;gBAC7H,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,OAAO,CAAC,IAAI,CAAC,6GAA6G,CAAC,CAAC;gBAC9H,CAAC;YACH,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,yBAAyB,SAAS,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YACrE,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;QAE/D,2DAA2D;QAC3D,8FAA8F;QAC9F,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,CAAC;QAE9F,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC9D,wEAAwE;gBACxE,IAAI,KAAmD,CAAC;gBAExD,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;oBACrD,KAAK,GAAG,YAAY,CAAC,kBAAkB,CAAC,CAAC;oBAEzC,oEAAoE;oBACpE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;wBACxB,KAAK,GAAG,YAAY,CAAC,YAAY,CAAC;4BAC1B,YAAY,CAAC,SAAS,CAAC;4BACvB,YAAY,CAAC,aAAa,CAAC;4BAC3B,YAAY,CAAC,KAAK,CAAC;4BACnB,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;oBACzC,CAAC;gBACH,CAAC;gBAED,8CAA8C;gBAC9C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBAC1C,6CAA6C;oBAC7C,MAAM,MAAM,GAAG,eAAe,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;oBAClD,UAAU,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;gBACrC,CAAC;YACH,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,iCAAiC,CAAC,CAAC;QACrG,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,8CAA8C,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACvH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,GAAsB,EACtB,WAAoB,EACpB,gBAAyB,IAAI,EAC7B,YAAoB,IAAI;IAExB,OAAO,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAC;IAEpE,0EAA0E;IAC1E,MAAM,aAAa,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;IAExD,oCAAoC;IACpC,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QACzD,IAAI,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACzB,aAAa,EAAE,CAAC;QAClB,CAAC;IACH,CAAC;IAED,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,wBAAwB,aAAa,4CAA4C,CAAC,CAAC;IACjG,CAAC;IAED,8DAA8D;IAC9D,IAAI,aAAa,EAAE,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,aAAa,GAAG,mBAAmB,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC1D,MAAM,cAAc,GAAG,IAAI,OAAO,CAAyB,CAAC,OAAO,EAAE,EAAE;gBACrE,UAAU,CAAC,GAAG,EAAE;oBACd,OAAO,CAAC,IAAI,CAAC,qDAAqD,SAAS,2BAA2B,CAAC,CAAC;oBACxG,OAAO,CAAC,EAAE,CAAC,CAAC;gBACd,CAAC,EAAE,SAAS,CAAC,CAAC;YAChB,CAAC,CAAC,CAAC;YAEH,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,CAAC;YAE/E,8BAA8B;YAC9B,IAAI,mBAAmB,GAAG,CAAC,CAAC;YAC5B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAC9D,IAAI,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC1B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;oBACzB,mBAAmB,EAAE,CAAC;gBACxB,CAAC;YACH,CAAC;YAED,IAAI,mBAAmB,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO,CAAC,GAAG,CAAC,wBAAwB,mBAAmB,iCAAiC,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,gEAAgE,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACzI,CAAC;IACH,CAAC;SAAM,CAAC;QACN,2CAA2C;QAC3C,mBAAmB,CAAC,GAAG,CAAC;aACrB,IAAI,CAAC,CAAC,kBAAkB,EAAE,EAAE;YAC3B,IAAI,mBAAmB,GAAG,CAAC,CAAC;YAC5B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAC9D,IAAI,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC1B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;oBACzB,mBAAmB,EAAE,CAAC;gBACxB,CAAC;YACH,CAAC;YACD,IAAI,mBAAmB,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO,CAAC,GAAG,CAAC,wBAAwB,mBAAmB,yDAAyD,CAAC,CAAC;YACpH,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACf,OAAO,CAAC,IAAI,CAAC,2DAA2D,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpI,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;AACpE,CAAC","sourcesContent":["/**\r\n * Environment Variable Loader\r\n * \r\n * This module handles loading environment variables from two sources:\r\n * 1. Developer-defined variables (from beam.env file)\r\n * 2. Beamable Config values (from API: namespace -> environment -> key-value pairs)\r\n * \r\n * All variables are injected into process.env before service initialization.\r\n */\r\n\r\nimport { existsSync, readFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport { createHash } from 'node:crypto';\r\nimport { URL } from 'node:url';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport { hostToHttpUrl } from './utils/urls.js';\r\n\r\n/**\r\n * Calculates Beamable signature for signed requests\r\n * Signature format: MD5(secret + pid + version + uriPathAndQuery + body) as Base64\r\n */\r\nfunction calculateSignature(\r\n pid: string,\r\n secret: string,\r\n uriPathAndQuery: string,\r\n body: string | null = null,\r\n version: string = '1'\r\n): string {\r\n let dataToSign = `${secret}${pid}${version}${uriPathAndQuery}`;\r\n if (body) {\r\n dataToSign += body;\r\n }\r\n const hash = createHash('md5').update(dataToSign, 'utf8').digest('base64');\r\n return hash;\r\n}\r\n\r\n/**\r\n * Loads developer-defined environment variables from beam.env file\r\n * Supports standard .env format: KEY=value\r\n * \r\n * This is exported for synchronous loading before logger initialization\r\n */\r\nexport function loadDeveloperEnvVarsSync(beamEnvPath?: string): void {\r\n const envVars = loadDeveloperEnvVars(beamEnvPath);\r\n \r\n // Inject immediately into process.env\r\n let injectedCount = 0;\r\n for (const [key, value] of Object.entries(envVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n injectedCount++;\r\n }\r\n }\r\n \r\n if (injectedCount > 0) {\r\n console.log(`[EnvLoader] Synchronously injected ${injectedCount} developer-defined variables from beam.env`);\r\n }\r\n}\r\n\r\n/**\r\n * Loads developer-defined environment variables from beam.env file\r\n * Supports standard .env format: KEY=value\r\n */\r\nfunction loadDeveloperEnvVars(beamEnvPath?: string): Record<string, string> {\r\n const envVars: Record<string, string> = {};\r\n \r\n // Try to find beam.env file\r\n const possiblePaths = [\r\n beamEnvPath,\r\n join(process.cwd(), 'beam.env'),\r\n join(process.cwd(), '.beam.env'),\r\n '/beam/service/beam.env', // Container path\r\n '/beam/service/.beam.env',\r\n ].filter((path): path is string => !!path && existsSync(path));\r\n\r\n if (possiblePaths.length === 0) {\r\n return envVars;\r\n }\r\n\r\n const envFilePath = possiblePaths[0];\r\n try {\r\n const content = readFileSync(envFilePath, 'utf-8');\r\n const lines = content.split('\\n');\r\n \r\n for (const line of lines) {\r\n const trimmed = line.trim();\r\n // Skip empty lines and comments\r\n if (!trimmed || trimmed.startsWith('#')) {\r\n continue;\r\n }\r\n \r\n // Parse KEY=value format\r\n const match = trimmed.match(/^([^=#]+)=(.*)$/);\r\n if (match) {\r\n const key = match[1].trim();\r\n let value = match[2].trim();\r\n \r\n // Remove quotes if present\r\n if ((value.startsWith('\"') && value.endsWith('\"')) || \r\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\r\n value = value.slice(1, -1);\r\n }\r\n \r\n if (key) {\r\n envVars[key] = value;\r\n }\r\n }\r\n }\r\n \r\n console.log(`[EnvLoader] Loaded ${Object.keys(envVars).length} variables from ${envFilePath}`);\r\n } catch (error) {\r\n console.warn(`[EnvLoader] Failed to load beam.env from ${envFilePath}:`, error instanceof Error ? error.message : String(error));\r\n }\r\n \r\n return envVars;\r\n}\r\n\r\n/**\r\n * Beamable Config API response structure\r\n */\r\ninterface BeamableConfigResponse {\r\n config?: {\r\n [key: string]: {\r\n [environment: string]: string | number | boolean | null;\r\n };\r\n };\r\n [key: string]: unknown;\r\n}\r\n\r\n/**\r\n * Fetches Beamable Config values from the API\r\n * \r\n * The endpoint can be configured via BEAM_CONFIG_API_PATH environment variable.\r\n * Default: /api/basic/realms/config\r\n * \r\n * Note: The exact endpoint format may vary. The API should return a structure like:\r\n * {\r\n * \"config\": {\r\n * \"key1\": {\r\n * \"environment\": \"value1\"\r\n * }\r\n * }\r\n * }\r\n * \r\n * Returns key-value pairs from the current realm's config namespace/environment\r\n */\r\nasync function fetchBeamableConfig(\r\n env: EnvironmentConfig,\r\n timeoutMs: number = 2000\r\n): Promise<Record<string, string>> {\r\n const configVars: Record<string, string> = {};\r\n \r\n // Skip if we don't have required credentials\r\n if (!env.cid || !env.pid) {\r\n console.log('[EnvLoader] Skipping Beamable Config fetch - missing CID or PID');\r\n return configVars;\r\n }\r\n\r\n // Check if we have an access token (preferred) or secret (fallback)\r\n const accessToken = process.env.ACCESS_TOKEN || process.env.BEAMABLE_TOKEN;\r\n const hasSecret = !!env.secret;\r\n \r\n if (!accessToken && !hasSecret) {\r\n console.log('[EnvLoader] Skipping Beamable Config fetch - missing ACCESS_TOKEN/BEAMABLE_TOKEN or SECRET');\r\n return configVars;\r\n }\r\n\r\n try {\r\n const apiUrl = hostToHttpUrl(env.host);\r\n // Allow endpoint to be configured via environment variable (for flexibility)\r\n // Default endpoint matches C# SDK and Portal: /basic/realms/config (not /api/basic/realms/config)\r\n const uriPath = process.env.BEAM_CONFIG_API_PATH || '/basic/realms/config';\r\n const configUrl = new URL(uriPath, apiUrl).toString();\r\n \r\n // Build headers - prefer token-based auth if available, fallback to signature-based\r\n const headers: Record<string, string> = {\r\n 'Content-Type': 'application/json',\r\n Accept: 'application/json',\r\n 'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,\r\n };\r\n \r\n if (accessToken) {\r\n // Use token-based authentication (matches Portal's $beamable.get())\r\n headers['Authorization'] = `Bearer ${accessToken}`;\r\n } else if (hasSecret && env.secret) {\r\n // Fallback to signature-based authentication\r\n const pathAndQuery = uriPath;\r\n const signature = calculateSignature(env.pid, env.secret, pathAndQuery, null, '1');\r\n headers['X-BEAM-SIGNATURE'] = signature;\r\n }\r\n \r\n console.log(`[EnvLoader] Fetching Beamable Config from ${configUrl}...`);\r\n console.log(`[EnvLoader] Using ${accessToken ? 'token-based' : 'signature-based'} authentication`);\r\n \r\n // Add timeout to fetch to prevent hanging requests\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\r\n \r\n let response: Response;\r\n try {\r\n response = await fetch(configUrl, {\r\n method: 'GET',\r\n headers,\r\n signal: controller.signal,\r\n });\r\n clearTimeout(timeoutId);\r\n } catch (error) {\r\n clearTimeout(timeoutId);\r\n if (error instanceof Error && error.name === 'AbortError') {\r\n console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms`);\r\n console.warn(`[EnvLoader] URL: ${configUrl}`);\r\n console.warn(`[EnvLoader] Auth method: ${accessToken ? 'token-based' : 'signature-based'}`);\r\n throw new Error(`Beamable Config fetch timed out after ${timeoutMs}ms`);\r\n }\r\n throw error;\r\n }\r\n\r\n if (!response.ok) {\r\n const errorText = await response.text().catch(() => 'Unknown error');\r\n console.warn(`[EnvLoader] Failed to fetch Beamable Config: ${response.status} ${response.statusText}`);\r\n console.warn(`[EnvLoader] URL: ${configUrl}`);\r\n console.warn(`[EnvLoader] Auth method: ${accessToken ? 'token-based' : 'signature-based'}`);\r\n if (response.status === 401 || response.status === 403) {\r\n console.warn(`[EnvLoader] Authentication failed. This endpoint requires token-based auth (ACCESS_TOKEN or BEAMABLE_TOKEN).`);\r\n if (!accessToken) {\r\n console.warn(`[EnvLoader] Tip: Set ACCESS_TOKEN or BEAMABLE_TOKEN in your environment to enable Beamable Config fetching.`);\r\n }\r\n }\r\n console.warn(`[EnvLoader] Response: ${errorText.substring(0, 500)}`);\r\n return configVars;\r\n }\r\n\r\n const data = (await response.json()) as BeamableConfigResponse;\r\n \r\n // Parse config structure: config[key][environment] = value\r\n // We need to determine the current environment - typically it's the PID or a specific env var\r\n const currentEnvironment = process.env.BEAM_ENVIRONMENT || process.env.ENVIRONMENT || env.pid;\r\n \r\n if (data.config) {\r\n for (const [key, environments] of Object.entries(data.config)) {\r\n // Try to get value for current environment, fallback to first available\r\n let value: string | number | boolean | null | undefined;\r\n \r\n if (environments && typeof environments === 'object') {\r\n value = environments[currentEnvironment];\r\n \r\n // If no value for current environment, try common environment names\r\n if (value === undefined) {\r\n value = environments['production'] || \r\n environments['staging'] || \r\n environments['development'] ||\r\n environments['dev'] ||\r\n Object.values(environments)[0];\r\n }\r\n }\r\n \r\n // Convert value to string and add to env vars\r\n if (value !== undefined && value !== null) {\r\n // Use BEAM_CONFIG_ prefix to avoid conflicts\r\n const envKey = `BEAM_CONFIG_${key.toUpperCase()}`;\r\n configVars[envKey] = String(value);\r\n }\r\n }\r\n \r\n console.log(`[EnvLoader] Loaded ${Object.keys(configVars).length} variables from Beamable Config`);\r\n } else {\r\n console.log('[EnvLoader] Beamable Config response contains no config object');\r\n }\r\n } catch (error) {\r\n console.warn('[EnvLoader] Failed to fetch Beamable Config:', error instanceof Error ? error.message : String(error));\r\n }\r\n \r\n return configVars;\r\n}\r\n\r\n/**\r\n * Loads all environment variables from both sources and injects them into process.env\r\n * \r\n * Priority (highest to lowest):\r\n * 1. Existing process.env values (never overwrite)\r\n * 2. Developer-defined variables (beam.env)\r\n * 3. Beamable Config values (API)\r\n * \r\n * @param env - Environment configuration (CID, PID, HOST, SECRET)\r\n * @param beamEnvPath - Optional path to beam.env file\r\n * @param waitForConfig - If true, waits up to timeoutMs for Beamable Config. If false, returns immediately after developer vars.\r\n * @param timeoutMs - Maximum time to wait for Beamable Config (default: 2000ms)\r\n */\r\nexport async function loadAndInjectEnvironmentVariables(\r\n env: EnvironmentConfig,\r\n beamEnvPath?: string,\r\n waitForConfig: boolean = true,\r\n timeoutMs: number = 2000\r\n): Promise<void> {\r\n console.log('[EnvLoader] Starting environment variable loading...');\r\n \r\n // Load developer-defined variables from beam.env (synchronous, immediate)\r\n const developerVars = loadDeveloperEnvVars(beamEnvPath);\r\n \r\n // Inject developer vars immediately\r\n let injectedCount = 0;\r\n for (const [key, value] of Object.entries(developerVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n injectedCount++;\r\n }\r\n }\r\n \r\n if (injectedCount > 0) {\r\n console.log(`[EnvLoader] Injected ${injectedCount} developer-defined variables from beam.env`);\r\n }\r\n \r\n // Fetch Beamable Config values (async, with optional timeout)\r\n if (waitForConfig) {\r\n try {\r\n const configPromise = fetchBeamableConfig(env, timeoutMs);\r\n const timeoutPromise = new Promise<Record<string, string>>((resolve) => {\r\n setTimeout(() => {\r\n console.warn(`[EnvLoader] Beamable Config fetch timed out after ${timeoutMs}ms, continuing without it`);\r\n resolve({});\r\n }, timeoutMs);\r\n });\r\n \r\n const beamableConfigVars = await Promise.race([configPromise, timeoutPromise]);\r\n \r\n // Inject Beamable Config vars\r\n let configInjectedCount = 0;\r\n for (const [key, value] of Object.entries(beamableConfigVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n configInjectedCount++;\r\n }\r\n }\r\n \r\n if (configInjectedCount > 0) {\r\n console.log(`[EnvLoader] Injected ${configInjectedCount} variables from Beamable Config`);\r\n }\r\n } catch (error) {\r\n console.warn('[EnvLoader] Error loading Beamable Config (continuing anyway):', error instanceof Error ? error.message : String(error));\r\n }\r\n } else {\r\n // Start fetch in background (non-blocking)\r\n fetchBeamableConfig(env)\r\n .then((beamableConfigVars) => {\r\n let configInjectedCount = 0;\r\n for (const [key, value] of Object.entries(beamableConfigVars)) {\r\n if (!(key in process.env)) {\r\n process.env[key] = value;\r\n configInjectedCount++;\r\n }\r\n }\r\n if (configInjectedCount > 0) {\r\n console.log(`[EnvLoader] Injected ${configInjectedCount} variables from Beamable Config (loaded asynchronously)`);\r\n }\r\n })\r\n .catch((error) => {\r\n console.warn('[EnvLoader] Failed to load Beamable Config in background:', error instanceof Error ? error.message : String(error));\r\n });\r\n }\r\n \r\n console.log('[EnvLoader] Environment variable loading completed');\r\n}\r\n\r\n"]}
package/dist/storage.cjs CHANGED
@@ -72,15 +72,64 @@ class StorageService {
72
72
  const database = await this.getDatabase(metadata.storageName, options);
73
73
  return database.collection(collectionName);
74
74
  }
75
+ normalizeConnectionString(connectionString) {
76
+ try {
77
+ const mongoUriRegex = /^(mongodb(?:\+srv)?:\/\/)(?:([^:@]+)(?::([^@]+))?@)?([^\/?]+)(?:\/([^?]*))?(?:\?(.*))?$/;
78
+ const match = connectionString.match(mongoUriRegex);
79
+ if (!match) {
80
+ this.logger.warn('Connection string does not match expected MongoDB URI format, using as-is');
81
+ return connectionString;
82
+ }
83
+ const [, protocol, username, password, host, database, options] = match;
84
+ if (!username) {
85
+ return connectionString;
86
+ }
87
+ let decodedUsername;
88
+ let decodedPassword;
89
+ try {
90
+ decodedUsername = decodeURIComponent(username);
91
+ }
92
+ catch {
93
+ decodedUsername = username;
94
+ }
95
+ if (password) {
96
+ try {
97
+ decodedPassword = decodeURIComponent(password);
98
+ }
99
+ catch {
100
+ decodedPassword = password;
101
+ }
102
+ }
103
+ const encodedUsername = encodeURIComponent(decodedUsername);
104
+ const encodedPassword = decodedPassword ? encodeURIComponent(decodedPassword) : undefined;
105
+ let normalized = `${protocol}${encodedUsername}`;
106
+ if (encodedPassword) {
107
+ normalized += `:${encodedPassword}`;
108
+ }
109
+ normalized += `@${host}`;
110
+ if (database) {
111
+ normalized += `/${database}`;
112
+ }
113
+ if (options) {
114
+ normalized += `?${options}`;
115
+ }
116
+ return normalized;
117
+ }
118
+ catch (error) {
119
+ this.logger.warn({ error: error instanceof Error ? error.message : String(error) }, 'Failed to normalize MongoDB connection string, using as-is');
120
+ return connectionString;
121
+ }
122
+ }
75
123
  async getMongoClient(connectionString) {
76
- if (this.clientCache.has(connectionString)) {
77
- return this.clientCache.get(connectionString);
124
+ const normalizedConnectionString = this.normalizeConnectionString(connectionString);
125
+ if (this.clientCache.has(normalizedConnectionString)) {
126
+ return this.clientCache.get(normalizedConnectionString);
78
127
  }
79
- const client = new mongodb_1.MongoClient(connectionString, {
128
+ const client = new mongodb_1.MongoClient(normalizedConnectionString, {
80
129
  maxPoolSize: 20,
81
130
  });
82
131
  await client.connect();
83
- this.clientCache.set(connectionString, client);
132
+ this.clientCache.set(normalizedConnectionString, client);
84
133
  return client;
85
134
  }
86
135
  async getConnectionString(storageName) {
package/dist/storage.d.ts CHANGED
@@ -34,6 +34,14 @@ export declare class StorageService {
34
34
  getDatabaseFor<T>(storageCtor: new () => T, options?: StorageConnectionOptions): Promise<Db>;
35
35
  getCollection<TDocument extends Document>(storageName: string, options?: StorageCollectionOptions): Promise<Collection<TDocument>>;
36
36
  getCollectionFor<TStorage, TDocument extends Document>(storageCtor: new () => TStorage, collectionCtor: new () => TDocument, options?: StorageCollectionOptions): Promise<Collection<TDocument>>;
37
+ /**
38
+ * Normalizes a MongoDB connection string by ensuring username and password are properly URL-encoded.
39
+ * This prevents "Password contains unescaped characters" errors when passwords contain special characters.
40
+ *
41
+ * Handles both mongodb:// and mongodb+srv:// formats.
42
+ * Safely handles already-encoded credentials by decoding first, then re-encoding.
43
+ */
44
+ private normalizeConnectionString;
37
45
  private getMongoClient;
38
46
  private getConnectionString;
39
47
  private fetchConnectionString;
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAe,KAAK,EAAE,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAOjE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,GAAG,SAAS,CAEhF;AAED,wBAAgB,4BAA4B,IAAI,eAAe,EAAE,CAEhE;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,aAAa,CAAC;IACzB,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,iBAAiB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAQD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,sBAAsB,CAAC,CAAS;gBAE5B,YAAY,EAAE,0BAA0B;IAO9C,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,EAAE,CAAC;IAkBrF,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,EAAE,CAAC;IAUhG,aAAa,CAAC,SAAS,SAAS,QAAQ,EAC5C,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAS3B,gBAAgB,CAAC,QAAQ,EAAE,SAAS,SAAS,QAAQ,EACzD,WAAW,EAAE,UAAU,QAAQ,EAC/B,cAAc,EAAE,UAAU,SAAS,EACnC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAYnB,cAAc;YAYd,mBAAmB;YAmBnB,qBAAqB;IAwBnC,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,oBAAoB;IAQtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ/B"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAe,KAAK,EAAE,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAOjE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,GAAG,SAAS,CAEhF;AAED,wBAAgB,4BAA4B,IAAI,eAAe,EAAE,CAEhE;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,aAAa,CAAC;IACzB,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,iBAAiB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAQD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,sBAAsB,CAAC,CAAS;gBAE5B,YAAY,EAAE,0BAA0B;IAO9C,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,EAAE,CAAC;IAkBrF,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,EAAE,CAAC;IAUhG,aAAa,CAAC,SAAS,SAAS,QAAQ,EAC5C,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAS3B,gBAAgB,CAAC,QAAQ,EAAE,SAAS,SAAS,QAAQ,EACzD,WAAW,EAAE,UAAU,QAAQ,EAC/B,cAAc,EAAE,UAAU,SAAS,EACnC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAYjC;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;YAqEnB,cAAc;YAed,mBAAmB;YAmBnB,qBAAqB;IAwBnC,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,oBAAoB;IAQtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ/B"}
package/dist/storage.js CHANGED
@@ -69,15 +69,83 @@ export class StorageService {
69
69
  const database = await this.getDatabase(metadata.storageName, options);
70
70
  return database.collection(collectionName);
71
71
  }
72
+ /**
73
+ * Normalizes a MongoDB connection string by ensuring username and password are properly URL-encoded.
74
+ * This prevents "Password contains unescaped characters" errors when passwords contain special characters.
75
+ *
76
+ * Handles both mongodb:// and mongodb+srv:// formats.
77
+ * Safely handles already-encoded credentials by decoding first, then re-encoding.
78
+ */
79
+ normalizeConnectionString(connectionString) {
80
+ try {
81
+ // Match MongoDB connection string format: mongodb:// or mongodb+srv://
82
+ // Format: mongodb[+srv]://[username:password@]host[:port][/database][?options]
83
+ const mongoUriRegex = /^(mongodb(?:\+srv)?:\/\/)(?:([^:@]+)(?::([^@]+))?@)?([^\/?]+)(?:\/([^?]*))?(?:\?(.*))?$/;
84
+ const match = connectionString.match(mongoUriRegex);
85
+ if (!match) {
86
+ // If it doesn't match the expected format, return as-is (might be a different format)
87
+ this.logger.warn('Connection string does not match expected MongoDB URI format, using as-is');
88
+ return connectionString;
89
+ }
90
+ const [, protocol, username, password, host, database, options] = match;
91
+ // If no username/password, return as-is
92
+ if (!username) {
93
+ return connectionString;
94
+ }
95
+ // Decode username and password first (in case they're already encoded)
96
+ // Then re-encode them properly to ensure special characters are handled
97
+ let decodedUsername;
98
+ let decodedPassword;
99
+ try {
100
+ decodedUsername = decodeURIComponent(username);
101
+ }
102
+ catch {
103
+ // If decoding fails, assume it's not encoded and use as-is
104
+ decodedUsername = username;
105
+ }
106
+ if (password) {
107
+ try {
108
+ decodedPassword = decodeURIComponent(password);
109
+ }
110
+ catch {
111
+ // If decoding fails, assume it's not encoded and use as-is
112
+ decodedPassword = password;
113
+ }
114
+ }
115
+ // Now encode them properly (encodeURIComponent handles all special characters)
116
+ const encodedUsername = encodeURIComponent(decodedUsername);
117
+ const encodedPassword = decodedPassword ? encodeURIComponent(decodedPassword) : undefined;
118
+ // Reconstruct the connection string
119
+ let normalized = `${protocol}${encodedUsername}`;
120
+ if (encodedPassword) {
121
+ normalized += `:${encodedPassword}`;
122
+ }
123
+ normalized += `@${host}`;
124
+ if (database) {
125
+ normalized += `/${database}`;
126
+ }
127
+ if (options) {
128
+ normalized += `?${options}`;
129
+ }
130
+ return normalized;
131
+ }
132
+ catch (error) {
133
+ // If parsing fails, log warning and return original
134
+ this.logger.warn({ error: error instanceof Error ? error.message : String(error) }, 'Failed to normalize MongoDB connection string, using as-is');
135
+ return connectionString;
136
+ }
137
+ }
72
138
  async getMongoClient(connectionString) {
73
- if (this.clientCache.has(connectionString)) {
74
- return this.clientCache.get(connectionString);
139
+ // Normalize the connection string to ensure username/password are properly encoded
140
+ const normalizedConnectionString = this.normalizeConnectionString(connectionString);
141
+ if (this.clientCache.has(normalizedConnectionString)) {
142
+ return this.clientCache.get(normalizedConnectionString);
75
143
  }
76
- const client = new MongoClient(connectionString, {
144
+ const client = new MongoClient(normalizedConnectionString, {
77
145
  maxPoolSize: 20,
78
146
  });
79
147
  await client.connect();
80
- this.clientCache.set(connectionString, client);
148
+ this.clientCache.set(normalizedConnectionString, client);
81
149
  return client;
82
150
  }
83
151
  async getConnectionString(storageName) {
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Logger } from 'pino';\r\nimport { MongoClient, type Db, type Collection, type Document } from 'mongodb';\r\nimport type { BoundBeamApi } from './services.js';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport type { HttpRequester } from 'beamable-sdk';\r\n\r\nexport interface StorageConnectionOptions {\r\n useCache?: boolean;\r\n}\r\n\r\nexport interface StorageCollectionOptions extends StorageConnectionOptions {\r\n collectionName?: string;\r\n}\r\n\r\nexport interface StorageMetadata {\r\n storageName: string;\r\n}\r\n\r\nconst STORAGE_OBJECT_METADATA = new Map<Function, StorageMetadata>();\r\n\r\nexport function StorageObject(storageName: string): ClassDecorator {\r\n if (!storageName || !storageName.trim()) {\r\n throw new Error('@StorageObject requires a non-empty storage name.');\r\n }\r\n return (target) => {\r\n STORAGE_OBJECT_METADATA.set(target, { storageName: storageName.trim() });\r\n };\r\n}\r\n\r\nexport function getStorageMetadata(target: Function): StorageMetadata | undefined {\r\n return STORAGE_OBJECT_METADATA.get(target);\r\n}\r\n\r\nexport function listRegisteredStorageObjects(): StorageMetadata[] {\r\n return Array.from(STORAGE_OBJECT_METADATA.values());\r\n}\r\n\r\ninterface StorageServiceDependencies {\r\n requester: HttpRequester;\r\n api: BoundBeamApi;\r\n env: EnvironmentConfig;\r\n logger: Logger;\r\n}\r\n\r\ninterface ConnectionStringResponse {\r\n connectionString: string;\r\n}\r\n\r\nconst CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';\r\n\r\nexport class StorageService {\r\n private readonly requester: HttpRequester;\r\n private readonly api: BoundBeamApi;\r\n private readonly env: EnvironmentConfig;\r\n private readonly logger: Logger;\r\n private readonly databaseCache = new Map<string, Db>();\r\n private readonly clientCache = new Map<string, MongoClient>();\r\n private cachedConnectionString?: string;\r\n\r\n constructor(dependencies: StorageServiceDependencies) {\r\n this.requester = dependencies.requester;\r\n this.api = dependencies.api;\r\n this.env = dependencies.env;\r\n this.logger = dependencies.logger.child({ component: 'StorageService' });\r\n }\r\n\r\n async getDatabase(storageName: string, options: StorageConnectionOptions = {}): Promise<Db> {\r\n const normalized = this.normalizeStorageName(storageName);\r\n if (!options.useCache) {\r\n this.databaseCache.delete(normalized);\r\n }\r\n const cached = this.databaseCache.get(normalized);\r\n if (cached) {\r\n return cached;\r\n }\r\n\r\n const connectionString = await this.getConnectionString(normalized);\r\n const client = await this.getMongoClient(connectionString);\r\n const databaseName = this.buildDatabaseName(normalized);\r\n const database = client.db(databaseName);\r\n this.databaseCache.set(normalized, database);\r\n return database;\r\n }\r\n\r\n async getDatabaseFor<T>(storageCtor: new () => T, options: StorageConnectionOptions = {}): Promise<Db> {\r\n const metadata = getStorageMetadata(storageCtor);\r\n if (!metadata) {\r\n throw new Error(\r\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\r\n );\r\n }\r\n return this.getDatabase(metadata.storageName, options);\r\n }\r\n\r\n async getCollection<TDocument extends Document>(\r\n storageName: string,\r\n options: StorageCollectionOptions = {},\r\n ): Promise<Collection<TDocument>> {\r\n const database = await this.getDatabase(storageName, options);\r\n const collectionName = options.collectionName?.trim();\r\n if (!collectionName) {\r\n throw new Error('Collection name must be provided when using getCollection with raw storage name.');\r\n }\r\n return database.collection<TDocument>(collectionName);\r\n }\r\n\r\n async getCollectionFor<TStorage, TDocument extends Document>(\r\n storageCtor: new () => TStorage,\r\n collectionCtor: new () => TDocument,\r\n options: StorageCollectionOptions = {},\r\n ): Promise<Collection<TDocument>> {\r\n const metadata = getStorageMetadata(storageCtor);\r\n if (!metadata) {\r\n throw new Error(\r\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\r\n );\r\n }\r\n const collectionName = options.collectionName?.trim() ?? collectionCtor.name;\r\n const database = await this.getDatabase(metadata.storageName, options);\r\n return database.collection<TDocument>(collectionName);\r\n }\r\n\r\n private async getMongoClient(connectionString: string): Promise<MongoClient> {\r\n if (this.clientCache.has(connectionString)) {\r\n return this.clientCache.get(connectionString) as MongoClient;\r\n }\r\n const client = new MongoClient(connectionString, {\r\n maxPoolSize: 20,\r\n });\r\n await client.connect();\r\n this.clientCache.set(connectionString, client);\r\n return client;\r\n }\r\n\r\n private async getConnectionString(storageName: string): Promise<string> {\r\n const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;\r\n const envValue = process.env[variableName];\r\n if (envValue && envValue.trim()) {\r\n return envValue.trim();\r\n }\r\n\r\n if (this.cachedConnectionString) {\r\n return this.cachedConnectionString;\r\n }\r\n\r\n const response = await this.fetchConnectionString();\r\n if (!response.connectionString || !response.connectionString.trim()) {\r\n throw new Error(`Connection string for storage \"${storageName}\" is empty.`);\r\n }\r\n this.cachedConnectionString = response.connectionString.trim();\r\n return this.cachedConnectionString;\r\n }\r\n\r\n private async fetchConnectionString(): Promise<ConnectionStringResponse> {\r\n if (typeof this.api.beamoGetStorageConnectionBasic === 'function') {\r\n const result = await this.api.beamoGetStorageConnectionBasic();\r\n if (result && typeof result === 'object' && 'body' in result) {\r\n return (result as { body: ConnectionStringResponse }).body;\r\n }\r\n return result as ConnectionStringResponse;\r\n }\r\n\r\n this.logger.warn(\r\n 'beamable-sdk does not expose beamoGetStorageConnectionBasic; falling back to manual requester call.',\r\n );\r\n const response = await this.requester.request({\r\n method: 'GET',\r\n url: '/basic/beamo/storage/connection',\r\n withAuth: true,\r\n });\r\n const body = response.body as ConnectionStringResponse | undefined;\r\n if (!body || typeof body.connectionString !== 'string') {\r\n throw new Error('Failed to retrieve Beamable storage connection string.');\r\n }\r\n return body;\r\n }\r\n\r\n private buildDatabaseName(storageName: string): string {\r\n const cid = this.sanitize(this.env.cid);\r\n const pid = this.sanitize(this.env.pid);\r\n const storage = this.sanitize(storageName);\r\n return `${cid}${pid}_${storage}`;\r\n }\r\n\r\n private sanitize(value: string): string {\r\n return value.replace(/[^A-Za-z0-9_]/g, '_');\r\n }\r\n\r\n private normalizeStorageName(storageName: string): string {\r\n const normalized = storageName.trim();\r\n if (!normalized) {\r\n throw new Error('Storage name cannot be empty.');\r\n }\r\n return normalized;\r\n }\r\n\r\n async dispose(): Promise<void> {\r\n for (const client of this.clientCache.values()) {\r\n await client.close();\r\n }\r\n this.clientCache.clear();\r\n this.databaseCache.clear();\r\n this.cachedConnectionString = undefined;\r\n }\r\n}\r\n\r\n"]}
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Logger } from 'pino';\r\nimport { MongoClient, type Db, type Collection, type Document } from 'mongodb';\r\nimport type { BoundBeamApi } from './services.js';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport type { HttpRequester } from 'beamable-sdk';\r\n\r\nexport interface StorageConnectionOptions {\r\n useCache?: boolean;\r\n}\r\n\r\nexport interface StorageCollectionOptions extends StorageConnectionOptions {\r\n collectionName?: string;\r\n}\r\n\r\nexport interface StorageMetadata {\r\n storageName: string;\r\n}\r\n\r\nconst STORAGE_OBJECT_METADATA = new Map<Function, StorageMetadata>();\r\n\r\nexport function StorageObject(storageName: string): ClassDecorator {\r\n if (!storageName || !storageName.trim()) {\r\n throw new Error('@StorageObject requires a non-empty storage name.');\r\n }\r\n return (target) => {\r\n STORAGE_OBJECT_METADATA.set(target, { storageName: storageName.trim() });\r\n };\r\n}\r\n\r\nexport function getStorageMetadata(target: Function): StorageMetadata | undefined {\r\n return STORAGE_OBJECT_METADATA.get(target);\r\n}\r\n\r\nexport function listRegisteredStorageObjects(): StorageMetadata[] {\r\n return Array.from(STORAGE_OBJECT_METADATA.values());\r\n}\r\n\r\ninterface StorageServiceDependencies {\r\n requester: HttpRequester;\r\n api: BoundBeamApi;\r\n env: EnvironmentConfig;\r\n logger: Logger;\r\n}\r\n\r\ninterface ConnectionStringResponse {\r\n connectionString: string;\r\n}\r\n\r\nconst CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';\r\n\r\nexport class StorageService {\r\n private readonly requester: HttpRequester;\r\n private readonly api: BoundBeamApi;\r\n private readonly env: EnvironmentConfig;\r\n private readonly logger: Logger;\r\n private readonly databaseCache = new Map<string, Db>();\r\n private readonly clientCache = new Map<string, MongoClient>();\r\n private cachedConnectionString?: string;\r\n\r\n constructor(dependencies: StorageServiceDependencies) {\r\n this.requester = dependencies.requester;\r\n this.api = dependencies.api;\r\n this.env = dependencies.env;\r\n this.logger = dependencies.logger.child({ component: 'StorageService' });\r\n }\r\n\r\n async getDatabase(storageName: string, options: StorageConnectionOptions = {}): Promise<Db> {\r\n const normalized = this.normalizeStorageName(storageName);\r\n if (!options.useCache) {\r\n this.databaseCache.delete(normalized);\r\n }\r\n const cached = this.databaseCache.get(normalized);\r\n if (cached) {\r\n return cached;\r\n }\r\n\r\n const connectionString = await this.getConnectionString(normalized);\r\n const client = await this.getMongoClient(connectionString);\r\n const databaseName = this.buildDatabaseName(normalized);\r\n const database = client.db(databaseName);\r\n this.databaseCache.set(normalized, database);\r\n return database;\r\n }\r\n\r\n async getDatabaseFor<T>(storageCtor: new () => T, options: StorageConnectionOptions = {}): Promise<Db> {\r\n const metadata = getStorageMetadata(storageCtor);\r\n if (!metadata) {\r\n throw new Error(\r\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\r\n );\r\n }\r\n return this.getDatabase(metadata.storageName, options);\r\n }\r\n\r\n async getCollection<TDocument extends Document>(\r\n storageName: string,\r\n options: StorageCollectionOptions = {},\r\n ): Promise<Collection<TDocument>> {\r\n const database = await this.getDatabase(storageName, options);\r\n const collectionName = options.collectionName?.trim();\r\n if (!collectionName) {\r\n throw new Error('Collection name must be provided when using getCollection with raw storage name.');\r\n }\r\n return database.collection<TDocument>(collectionName);\r\n }\r\n\r\n async getCollectionFor<TStorage, TDocument extends Document>(\r\n storageCtor: new () => TStorage,\r\n collectionCtor: new () => TDocument,\r\n options: StorageCollectionOptions = {},\r\n ): Promise<Collection<TDocument>> {\r\n const metadata = getStorageMetadata(storageCtor);\r\n if (!metadata) {\r\n throw new Error(\r\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\r\n );\r\n }\r\n const collectionName = options.collectionName?.trim() ?? collectionCtor.name;\r\n const database = await this.getDatabase(metadata.storageName, options);\r\n return database.collection<TDocument>(collectionName);\r\n }\r\n\r\n /**\r\n * Normalizes a MongoDB connection string by ensuring username and password are properly URL-encoded.\r\n * This prevents \"Password contains unescaped characters\" errors when passwords contain special characters.\r\n * \r\n * Handles both mongodb:// and mongodb+srv:// formats.\r\n * Safely handles already-encoded credentials by decoding first, then re-encoding.\r\n */\r\n private normalizeConnectionString(connectionString: string): string {\r\n try {\r\n // Match MongoDB connection string format: mongodb:// or mongodb+srv://\r\n // Format: mongodb[+srv]://[username:password@]host[:port][/database][?options]\r\n const mongoUriRegex = /^(mongodb(?:\\+srv)?:\\/\\/)(?:([^:@]+)(?::([^@]+))?@)?([^\\/?]+)(?:\\/([^?]*))?(?:\\?(.*))?$/;\r\n const match = connectionString.match(mongoUriRegex);\r\n \r\n if (!match) {\r\n // If it doesn't match the expected format, return as-is (might be a different format)\r\n this.logger.warn('Connection string does not match expected MongoDB URI format, using as-is');\r\n return connectionString;\r\n }\r\n\r\n const [, protocol, username, password, host, database, options] = match;\r\n \r\n // If no username/password, return as-is\r\n if (!username) {\r\n return connectionString;\r\n }\r\n\r\n // Decode username and password first (in case they're already encoded)\r\n // Then re-encode them properly to ensure special characters are handled\r\n let decodedUsername: string;\r\n let decodedPassword: string | undefined;\r\n \r\n try {\r\n decodedUsername = decodeURIComponent(username);\r\n } catch {\r\n // If decoding fails, assume it's not encoded and use as-is\r\n decodedUsername = username;\r\n }\r\n \r\n if (password) {\r\n try {\r\n decodedPassword = decodeURIComponent(password);\r\n } catch {\r\n // If decoding fails, assume it's not encoded and use as-is\r\n decodedPassword = password;\r\n }\r\n }\r\n\r\n // Now encode them properly (encodeURIComponent handles all special characters)\r\n const encodedUsername = encodeURIComponent(decodedUsername);\r\n const encodedPassword = decodedPassword ? encodeURIComponent(decodedPassword) : undefined;\r\n\r\n // Reconstruct the connection string\r\n let normalized = `${protocol}${encodedUsername}`;\r\n if (encodedPassword) {\r\n normalized += `:${encodedPassword}`;\r\n }\r\n normalized += `@${host}`;\r\n if (database) {\r\n normalized += `/${database}`;\r\n }\r\n if (options) {\r\n normalized += `?${options}`;\r\n }\r\n\r\n return normalized;\r\n } catch (error) {\r\n // If parsing fails, log warning and return original\r\n this.logger.warn(\r\n { error: error instanceof Error ? error.message : String(error) },\r\n 'Failed to normalize MongoDB connection string, using as-is'\r\n );\r\n return connectionString;\r\n }\r\n }\r\n\r\n private async getMongoClient(connectionString: string): Promise<MongoClient> {\r\n // Normalize the connection string to ensure username/password are properly encoded\r\n const normalizedConnectionString = this.normalizeConnectionString(connectionString);\r\n \r\n if (this.clientCache.has(normalizedConnectionString)) {\r\n return this.clientCache.get(normalizedConnectionString) as MongoClient;\r\n }\r\n const client = new MongoClient(normalizedConnectionString, {\r\n maxPoolSize: 20,\r\n });\r\n await client.connect();\r\n this.clientCache.set(normalizedConnectionString, client);\r\n return client;\r\n }\r\n\r\n private async getConnectionString(storageName: string): Promise<string> {\r\n const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;\r\n const envValue = process.env[variableName];\r\n if (envValue && envValue.trim()) {\r\n return envValue.trim();\r\n }\r\n\r\n if (this.cachedConnectionString) {\r\n return this.cachedConnectionString;\r\n }\r\n\r\n const response = await this.fetchConnectionString();\r\n if (!response.connectionString || !response.connectionString.trim()) {\r\n throw new Error(`Connection string for storage \"${storageName}\" is empty.`);\r\n }\r\n this.cachedConnectionString = response.connectionString.trim();\r\n return this.cachedConnectionString;\r\n }\r\n\r\n private async fetchConnectionString(): Promise<ConnectionStringResponse> {\r\n if (typeof this.api.beamoGetStorageConnectionBasic === 'function') {\r\n const result = await this.api.beamoGetStorageConnectionBasic();\r\n if (result && typeof result === 'object' && 'body' in result) {\r\n return (result as { body: ConnectionStringResponse }).body;\r\n }\r\n return result as ConnectionStringResponse;\r\n }\r\n\r\n this.logger.warn(\r\n 'beamable-sdk does not expose beamoGetStorageConnectionBasic; falling back to manual requester call.',\r\n );\r\n const response = await this.requester.request({\r\n method: 'GET',\r\n url: '/basic/beamo/storage/connection',\r\n withAuth: true,\r\n });\r\n const body = response.body as ConnectionStringResponse | undefined;\r\n if (!body || typeof body.connectionString !== 'string') {\r\n throw new Error('Failed to retrieve Beamable storage connection string.');\r\n }\r\n return body;\r\n }\r\n\r\n private buildDatabaseName(storageName: string): string {\r\n const cid = this.sanitize(this.env.cid);\r\n const pid = this.sanitize(this.env.pid);\r\n const storage = this.sanitize(storageName);\r\n return `${cid}${pid}_${storage}`;\r\n }\r\n\r\n private sanitize(value: string): string {\r\n return value.replace(/[^A-Za-z0-9_]/g, '_');\r\n }\r\n\r\n private normalizeStorageName(storageName: string): string {\r\n const normalized = storageName.trim();\r\n if (!normalized) {\r\n throw new Error('Storage name cannot be empty.');\r\n }\r\n return normalized;\r\n }\r\n\r\n async dispose(): Promise<void> {\r\n for (const client of this.clientCache.values()) {\r\n await client.close();\r\n }\r\n this.clientCache.clear();\r\n this.databaseCache.clear();\r\n this.cachedConnectionString = undefined;\r\n }\r\n}\r\n\r\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omen.foundation/node-microservice-runtime",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
4
4
  "description": "Beamable microservice runtime for Node.js/TypeScript services.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",