@omen.foundation/node-microservice-runtime 0.1.75 → 0.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.d.ts CHANGED
@@ -20,6 +20,13 @@ export declare class MicroserviceRuntime {
20
20
  private handleEvent;
21
21
  private toRequestContext;
22
22
  private findServiceForPath;
23
+ /**
24
+ * Parses a query string into a record of parameter names to values.
25
+ * Handles multiple values for the same parameter name by storing them as an array.
26
+ * @param queryString The query string (without the leading '?')
27
+ * @returns A record mapping parameter names to their values (single string or array of strings)
28
+ */
29
+ private parseQueryString;
23
30
  private dispatch;
24
31
  private buildInvocationArguments;
25
32
  private sendSuccessResponse;
@@ -30,6 +37,12 @@ export declare class MicroserviceRuntime {
30
37
  private printHelpfulUrls;
31
38
  private initializeDependencyProviders;
32
39
  private createRequestScope;
40
+ /**
41
+ * Extracts the path portion without the query string.
42
+ * @param path The full path potentially containing a query string
43
+ * @returns The path without the query string
44
+ */
45
+ private getPathWithoutQuery;
33
46
  }
34
47
  export declare function runMicroservice(): Promise<void>;
35
48
  interface GenerateOpenApiOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,YAAY,CAAC;AA2BpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,OAAO,CAAC,OAAO,CAAkB;gBAErB,GAAG,CAAC,EAAE,iBAAiB;IA4H7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6DtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YASjB,sBAAsB;YA2CtB,qBAAqB;YAarB,eAAe;YA+Gf,WAAW;IA8BzB,OAAO,CAAC,gBAAgB;IAkFxB,OAAO,CAAC,kBAAkB;YAUZ,QAAQ;IAsDtB,OAAO,CAAC,wBAAwB;YAmBlB,mBAAmB;YAiBnB,sBAAsB;YAStB,wBAAwB;YAoBxB,mBAAmB;IA2GjC,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,gBAAgB;YA+BV,6BAA6B;IA6B3C,OAAO,CAAC,kBAAkB;CAK3B;AA+GD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAoDrD;AAED,UAAU,sBAAsB;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,4CAA4C,CAC1D,SAAS,GAAE,OAAO,CAAC,iBAAiB,CAAM,EAC1C,OAAO,GAAE,sBAA2B,GACnC,OAAO,CA6BT"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,YAAY,CAAC;AA2BpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,OAAO,CAAC,OAAO,CAAkB;gBAErB,GAAG,CAAC,EAAE,iBAAiB;IA2H7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6DtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YASjB,sBAAsB;YA2CtB,qBAAqB;YAarB,eAAe;YA+Gf,WAAW;IA8BzB,OAAO,CAAC,gBAAgB;IAuFxB,OAAO,CAAC,kBAAkB;IAW1B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;YA+BV,QAAQ;IAwDtB,OAAO,CAAC,wBAAwB;YAmBlB,mBAAmB;YAiBnB,sBAAsB;YAStB,wBAAwB;YAsBxB,mBAAmB;IA6GjC,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,gBAAgB;YA+BV,6BAA6B;IA6B3C,OAAO,CAAC,kBAAkB;IAO1B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAI5B;AA+GD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAoDrD;AAED,UAAU,sBAAsB;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,4CAA4C,CAC1D,SAAS,GAAE,OAAO,CAAC,iBAAiB,CAAM,EAC1C,OAAO,GAAE,sBAA2B,GACnC,OAAO,CA6BT"}
package/dist/runtime.js CHANGED
@@ -4,7 +4,7 @@ import { GatewayRequester } from './requester.js';
4
4
  import { AuthManager } from './auth.js';
5
5
  import { createLogger } from './logger.js';
6
6
  import { loadEnvironmentConfig } from './env.js';
7
- import { loadAndInjectEnvironmentVariables } from './env-loader.js';
7
+ import { loadAndInjectEnvironmentVariables, loadDeveloperEnvVarsSync } from './env-loader.js';
8
8
  import { startCollectorAndWaitForReady } from './collector-manager.js';
9
9
  import pino from 'pino';
10
10
  // Removed deasync - using non-blocking async pattern instead
@@ -33,7 +33,10 @@ export class MicroserviceRuntime {
33
33
  constructor(env) {
34
34
  this.env = env ?? loadEnvironmentConfig();
35
35
  const envConfig = this.env; // Capture for async IIFE to satisfy TypeScript
36
- // STEP 1: Create minimal console logger for startup messages (before collector setup)
36
+ // STEP 1: Load developer-defined environment variables SYNCHRONOUSLY before logger creation
37
+ // This ensures BetterStack and other env vars are available when the logger initializes
38
+ loadDeveloperEnvVarsSync();
39
+ // STEP 2: Create minimal console logger for startup messages (before collector setup)
37
40
  // This ensures we have logging available immediately, even before collector is ready
38
41
  const startupLogger = pino({
39
42
  name: 'beamable-runtime-startup',
@@ -41,24 +44,19 @@ export class MicroserviceRuntime {
41
44
  }, process.stdout);
42
45
  // Display runtime version at startup (VERSION is imported synchronously at top of file)
43
46
  startupLogger.info(`Starting Beamable Node microservice runtime (version: ${VERSION}).`);
44
- // STEP 1.5: Load additional environment variables (developer-defined + Beamable Config)
45
- // This runs synchronously but with a timeout - we wait up to 2 seconds for Beamable Config
46
- // Developer-defined vars (beam.env) are loaded immediately
47
- // Beamable Config is fetched via API with a 2-second timeout to avoid blocking startup
48
- // Note: This is wrapped in an IIFE to allow await at the top level of constructor logic
49
- // We use a fire-and-forget pattern: load vars but don't block service initialization
47
+ // STEP 2.5: Load Beamable Config asynchronously (in background, non-blocking)
48
+ // Developer-defined vars are already loaded synchronously above
49
+ // Beamable Config is fetched via API with a 2-second timeout
50
50
  (async () => {
51
51
  try {
52
- startupLogger.info('Loading environment variables from beam.env and Beamable Config...');
52
+ startupLogger.info('Loading Beamable Config environment variables...');
53
53
  await loadAndInjectEnvironmentVariables(envConfig, undefined, true, 2000);
54
- startupLogger.info('Environment variable loading completed');
54
+ startupLogger.info('Beamable Config loading completed');
55
55
  }
56
56
  catch (error) {
57
- startupLogger.warn(`Environment variable loading completed with warnings: ${error instanceof Error ? error.message : String(error)}`);
57
+ startupLogger.warn(`Beamable Config loading completed with warnings: ${error instanceof Error ? error.message : String(error)}`);
58
58
  }
59
59
  })();
60
- // Continue immediately - env vars will be available as they load
61
- // Developer vars are already injected, Beamable Config will arrive within 2 seconds
62
60
  // STEP 2: Get registered services to extract service name
63
61
  const registered = listRegisteredServices();
64
62
  if (registered.length === 0) {
@@ -386,6 +384,9 @@ export class MicroserviceRuntime {
386
384
  const method = envelope.method ?? 'post';
387
385
  const userId = typeof envelope.from === 'number' ? envelope.from : 0;
388
386
  const headers = envelope.headers ?? {};
387
+ // Parse query parameters from path (e.g., /service/route?param1=value1&param2=value2)
388
+ const [pathWithoutQuery, queryString] = path.split('?', 2);
389
+ const query = this.parseQueryString(queryString ?? '');
389
390
  // Extract scopes from envelope.scopes array
390
391
  // Note: X-DE-SCOPE header contains CID.PID, not scope values
391
392
  // The gateway sends scopes in envelope.scopes array
@@ -395,7 +396,7 @@ export class MicroserviceRuntime {
395
396
  let scopes = normalizeScopes(envelopeScopes);
396
397
  // If this is an admin endpoint and no scopes are provided, infer admin scope
397
398
  // The gateway may not always send scopes for admin routes
398
- const pathLower = path.toLowerCase();
399
+ const pathLower = pathWithoutQuery.toLowerCase();
399
400
  if (pathLower.includes('/admin/') && scopes.size === 0) {
400
401
  scopes = normalizeScopes(['admin']);
401
402
  }
@@ -427,9 +428,9 @@ export class MicroserviceRuntime {
427
428
  payload = rawPayload;
428
429
  }
429
430
  }
430
- const targetService = this.findServiceForPath(path);
431
+ const targetService = this.findServiceForPath(pathWithoutQuery);
431
432
  const services = this.serviceManager.createFacade(userId, scopes, targetService?.definition.name);
432
- const provider = this.createRequestScope(path, targetService);
433
+ const provider = this.createRequestScope(pathWithoutQuery, targetService);
433
434
  const context = {
434
435
  id: envelope.id,
435
436
  path,
@@ -438,6 +439,7 @@ export class MicroserviceRuntime {
438
439
  userId,
439
440
  payload,
440
441
  body,
442
+ query,
441
443
  scopes,
442
444
  headers,
443
445
  cid: this.env.cid,
@@ -461,14 +463,51 @@ export class MicroserviceRuntime {
461
463
  findServiceForPath(path) {
462
464
  // Gateway sends paths with lowercase service names, so we need case-insensitive matching
463
465
  // Match by comparing lowercase versions to handle gateway's lowercase path format
466
+ // Note: path should already have query string stripped before calling this method
464
467
  const pathLower = path.toLowerCase();
465
468
  return this.services.find((service) => {
466
469
  const qualifiedNameLower = service.definition.qualifiedName.toLowerCase();
467
470
  return pathLower.startsWith(`${qualifiedNameLower}/`);
468
471
  });
469
472
  }
473
+ /**
474
+ * Parses a query string into a record of parameter names to values.
475
+ * Handles multiple values for the same parameter name by storing them as an array.
476
+ * @param queryString The query string (without the leading '?')
477
+ * @returns A record mapping parameter names to their values (single string or array of strings)
478
+ */
479
+ parseQueryString(queryString) {
480
+ if (!queryString || queryString.trim().length === 0) {
481
+ return {};
482
+ }
483
+ const params = {};
484
+ const pairs = queryString.split('&');
485
+ for (const pair of pairs) {
486
+ const [key, value = ''] = pair.split('=', 2);
487
+ if (key) {
488
+ const decodedKey = decodeURIComponent(key);
489
+ const decodedValue = decodeURIComponent(value);
490
+ // If the key already exists, convert to array or append to existing array
491
+ if (decodedKey in params) {
492
+ const existing = params[decodedKey];
493
+ if (Array.isArray(existing)) {
494
+ existing.push(decodedValue);
495
+ }
496
+ else {
497
+ params[decodedKey] = [existing, decodedValue];
498
+ }
499
+ }
500
+ else {
501
+ params[decodedKey] = decodedValue;
502
+ }
503
+ }
504
+ }
505
+ return params;
506
+ }
470
507
  async dispatch(ctx) {
471
- const service = this.findServiceForPath(ctx.path);
508
+ // Extract path without query string for routing
509
+ const pathWithoutQuery = this.getPathWithoutQuery(ctx.path);
510
+ const service = this.findServiceForPath(pathWithoutQuery);
472
511
  if (!service) {
473
512
  throw new UnknownRouteError(ctx.path);
474
513
  }
@@ -479,7 +518,7 @@ export class MicroserviceRuntime {
479
518
  return;
480
519
  }
481
520
  // Extract route from path - handle case-insensitive path matching
482
- const pathLower = ctx.path.toLowerCase();
521
+ const pathLower = pathWithoutQuery.toLowerCase();
483
522
  const qualifiedNameLower = service.definition.qualifiedName.toLowerCase();
484
523
  const route = pathLower.substring(qualifiedNameLower.length + 1);
485
524
  const metadata = service.definition.callables.get(route);
@@ -554,14 +593,16 @@ export class MicroserviceRuntime {
554
593
  }
555
594
  async tryHandleFederationRoute(ctx, service) {
556
595
  // Gateway sends paths with lowercase service names, so we need case-insensitive matching
557
- const pathLower = ctx.path.toLowerCase();
596
+ // Extract path without query string for routing
597
+ const pathWithoutQuery = this.getPathWithoutQuery(ctx.path);
598
+ const pathLower = pathWithoutQuery.toLowerCase();
558
599
  const qualifiedNameLower = service.definition.qualifiedName.toLowerCase();
559
600
  const prefixLower = `${qualifiedNameLower}/`;
560
601
  if (!pathLower.startsWith(prefixLower)) {
561
602
  return false;
562
603
  }
563
604
  // Extract relative path - use lowercase length since gateway sends lowercase paths
564
- const relativePath = ctx.path.substring(qualifiedNameLower.length + 1);
605
+ const relativePath = pathWithoutQuery.substring(qualifiedNameLower.length + 1);
565
606
  const handler = service.federationRegistry.resolve(relativePath);
566
607
  if (!handler) {
567
608
  return false;
@@ -573,7 +614,9 @@ export class MicroserviceRuntime {
573
614
  async tryHandleAdminRoute(ctx, service) {
574
615
  // Gateway sends paths with lowercase service names, so we need case-insensitive matching
575
616
  // Check if path starts with the admin prefix (case-insensitive)
576
- const pathLower = ctx.path.toLowerCase();
617
+ // Extract path without query string for routing
618
+ const pathWithoutQuery = this.getPathWithoutQuery(ctx.path);
619
+ const pathLower = pathWithoutQuery.toLowerCase();
577
620
  const adminPrefixLower = `${service.definition.qualifiedName.toLowerCase()}/admin/`;
578
621
  if (!pathLower.startsWith(adminPrefixLower)) {
579
622
  return false;
@@ -730,10 +773,20 @@ export class MicroserviceRuntime {
730
773
  }
731
774
  }
732
775
  createRequestScope(path, service) {
776
+ // Path should already have query string stripped before calling this method
733
777
  const targetService = service ?? this.findServiceForPath(path);
734
778
  const provider = targetService?.provider ?? new DependencyBuilder().build();
735
779
  return provider.createScope();
736
780
  }
781
+ /**
782
+ * Extracts the path portion without the query string.
783
+ * @param path The full path potentially containing a query string
784
+ * @returns The path without the query string
785
+ */
786
+ getPathWithoutQuery(path) {
787
+ const [pathWithoutQuery] = path.split('?', 2);
788
+ return pathWithoutQuery;
789
+ }
737
790
  }
738
791
  function enforceAccess(access, ctx) {
739
792
  switch (access) {