@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.
@@ -0,0 +1,841 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MicroserviceRuntime = void 0;
7
+ exports.runMicroservice = runMicroservice;
8
+ exports.generateOpenApiDocumentForRegisteredServices = generateOpenApiDocumentForRegisteredServices;
9
+ const node_crypto_1 = require("node:crypto");
10
+ const websocket_js_1 = require("./websocket.js");
11
+ const requester_js_1 = require("./requester.js");
12
+ const auth_js_1 = require("./auth.js");
13
+ const logger_js_1 = require("./logger.js");
14
+ const env_js_1 = require("./env.js");
15
+ const env_loader_js_1 = require("./env-loader.js");
16
+ const collector_manager_js_1 = require("./collector-manager.js");
17
+ const pino_1 = __importDefault(require("pino"));
18
+ const decorators_js_1 = require("./decorators.js");
19
+ const docs_js_1 = require("./docs.js");
20
+ const index_js_1 = require("./index.js");
21
+ const discovery_js_1 = require("./discovery.js");
22
+ const errors_js_1 = require("./errors.js");
23
+ const services_js_1 = require("./services.js");
24
+ const dependency_js_1 = require("./dependency.js");
25
+ const urls_js_1 = require("./utils/urls.js");
26
+ const federation_js_1 = require("./federation.js");
27
+ const node_http_1 = require("node:http");
28
+ class MicroserviceRuntime {
29
+ constructor(env) {
30
+ this.microServiceId = (0, node_crypto_1.randomUUID)();
31
+ this.isReady = false;
32
+ this.env = env !== null && env !== void 0 ? env : (0, env_js_1.loadEnvironmentConfig)();
33
+ const envConfig = this.env;
34
+ (0, env_loader_js_1.loadDeveloperEnvVarsSync)();
35
+ const startupLogger = (0, pino_1.default)({
36
+ name: 'beamable-runtime-startup',
37
+ level: 'info',
38
+ }, process.stdout);
39
+ startupLogger.info(`Starting Beamable Node microservice runtime (version: ${index_js_1.VERSION}).`);
40
+ (async () => {
41
+ try {
42
+ startupLogger.info('Loading Beamable Config environment variables...');
43
+ await (0, env_loader_js_1.loadAndInjectEnvironmentVariables)(envConfig, undefined, true, 2000);
44
+ startupLogger.info('Beamable Config loading completed');
45
+ }
46
+ catch (error) {
47
+ startupLogger.warn(`Beamable Config loading completed with warnings: ${error instanceof Error ? error.message : String(error)}`);
48
+ }
49
+ })();
50
+ const registered = (0, decorators_js_1.listRegisteredServices)();
51
+ if (registered.length === 0) {
52
+ throw new Error('No microservices registered. Use the @Microservice decorator to register at least one class.');
53
+ }
54
+ const primaryService = registered[0];
55
+ const qualifiedServiceName = `micro_${primaryService.qualifiedName}`;
56
+ startupLogger.info('Setting up OpenTelemetry collector in background (non-blocking)...');
57
+ this.logger = (0, logger_js_1.createLogger)(this.env, {
58
+ name: 'beamable-node-microservice',
59
+ serviceName: primaryService.name,
60
+ qualifiedServiceName: qualifiedServiceName,
61
+ });
62
+ (0, collector_manager_js_1.startCollectorAndWaitForReady)(this.env)
63
+ .then((endpoint) => {
64
+ if (endpoint) {
65
+ this.logger.info(`Collector ready at ${endpoint}, upgrading to structured logger for Portal logs...`);
66
+ this.logger = (0, logger_js_1.createLogger)(this.env, {
67
+ name: 'beamable-node-microservice',
68
+ serviceName: primaryService.name,
69
+ qualifiedServiceName: qualifiedServiceName,
70
+ otlpEndpoint: endpoint,
71
+ });
72
+ this.logger.info('Portal logs enabled - structured logs will now appear in Beamable Portal');
73
+ }
74
+ else {
75
+ this.logger.warn('Collector setup completed but no endpoint was returned. Continuing with console logs. Portal logs will not be available.');
76
+ }
77
+ })
78
+ .catch((error) => {
79
+ const errorMsg = error instanceof Error ? error.message : String(error);
80
+ this.logger.error(`Failed to setup collector: ${errorMsg}. Continuing with console logs. Portal logs will not be available.`);
81
+ });
82
+ this.serviceManager = new services_js_1.BeamableServiceManager(this.env, this.logger);
83
+ this.services = registered.map((definition) => {
84
+ const instance = new definition.ctor();
85
+ const configureHandlers = (0, decorators_js_1.getConfigureServicesHandlers)(definition.ctor);
86
+ const initializeHandlers = (0, decorators_js_1.getInitializeServicesHandlers)(definition.ctor);
87
+ const logger = this.logger.child({ service: definition.name });
88
+ const federationRegistry = new federation_js_1.FederationRegistry(logger);
89
+ const decoratedFederations = (0, federation_js_1.getFederationComponents)(definition.ctor);
90
+ for (const component of decoratedFederations) {
91
+ federationRegistry.register(component);
92
+ }
93
+ const inventoryMetadata = (0, federation_js_1.getFederatedInventoryMetadata)(definition.ctor);
94
+ if (inventoryMetadata.length > 0) {
95
+ federationRegistry.registerInventoryHandlers(instance, inventoryMetadata);
96
+ }
97
+ this.serviceManager.registerFederationRegistry(definition.name, federationRegistry);
98
+ return { definition, instance, configureHandlers, initializeHandlers, logger, federationRegistry };
99
+ });
100
+ const socketUrl = this.env.host.endsWith('/socket') ? this.env.host : `${this.env.host}/socket`;
101
+ this.webSocket = new websocket_js_1.BeamableWebSocket({ url: socketUrl, logger: this.logger });
102
+ this.webSocket.on('message', (payload) => {
103
+ this.logger.debug({ payload }, 'Runtime observed websocket frame.');
104
+ });
105
+ this.requester = new requester_js_1.GatewayRequester(this.webSocket, this.logger);
106
+ this.authManager = new auth_js_1.AuthManager(this.env, this.requester);
107
+ this.requester.on('event', (envelope) => this.handleEvent(envelope));
108
+ if (!isRunningInContainer() && this.services.length > 0) {
109
+ this.discovery = new discovery_js_1.DiscoveryBroadcaster({
110
+ env: this.env,
111
+ serviceName: this.services[0].definition.name,
112
+ routingKey: this.env.routingKey,
113
+ logger: this.logger.child({ component: 'DiscoveryBroadcaster' }),
114
+ });
115
+ }
116
+ }
117
+ async start() {
118
+ var _a;
119
+ debugLog('[BEAMABLE-NODE] MicroserviceRuntime.start() called');
120
+ debugLog(`[BEAMABLE-NODE] Service count: ${this.services.length}`);
121
+ this.printHelpfulUrls(this.services[0]);
122
+ this.logger.info('Starting Beamable Node microservice runtime.');
123
+ debugLog('[BEAMABLE-NODE] Starting health check server...');
124
+ await this.startHealthCheckServer();
125
+ debugLog('[BEAMABLE-NODE] Health check server started');
126
+ try {
127
+ this.logger.info('Connecting to Beamable gateway...');
128
+ await this.webSocket.connect();
129
+ await new Promise((resolve) => setTimeout(resolve, 250));
130
+ this.logger.info('Authenticating with Beamable...');
131
+ await this.authManager.authenticate();
132
+ this.logger.info('Initializing Beamable SDK services...');
133
+ await this.serviceManager.initialize();
134
+ this.logger.info('Initializing dependency providers...');
135
+ await this.initializeDependencyProviders();
136
+ this.logger.info('Registering service with gateway...');
137
+ await this.registerService();
138
+ this.logger.info('Starting discovery broadcaster...');
139
+ await ((_a = this.discovery) === null || _a === void 0 ? void 0 : _a.start());
140
+ this.isReady = true;
141
+ this.logger.info('Beamable microservice runtime is ready to accept traffic.');
142
+ }
143
+ catch (error) {
144
+ debugLog('[BEAMABLE-NODE] FATAL ERROR during startup:');
145
+ debugLog(`[BEAMABLE-NODE] Error message: ${error instanceof Error ? error.message : String(error)}`);
146
+ debugLog(`[BEAMABLE-NODE] Error stack: ${error instanceof Error ? error.stack : 'No stack trace'}`);
147
+ debugLog(`[BEAMABLE-NODE] isReady: ${this.isReady}`);
148
+ this.logger.error({
149
+ err: error,
150
+ errorMessage: error instanceof Error ? error.message : String(error),
151
+ errorStack: error instanceof Error ? error.stack : undefined,
152
+ isReady: this.isReady,
153
+ }, 'Failed to fully initialize microservice runtime. Health check will continue to return 503 until initialization completes.');
154
+ this.isReady = false;
155
+ }
156
+ }
157
+ async shutdown() {
158
+ var _a;
159
+ this.logger.info('Shutting down microservice runtime.');
160
+ this.isReady = false;
161
+ (_a = this.discovery) === null || _a === void 0 ? void 0 : _a.stop();
162
+ await this.stopHealthCheckServer();
163
+ this.requester.dispose();
164
+ await this.webSocket.close();
165
+ }
166
+ async startHealthCheckServer() {
167
+ const healthPort = this.env.healthPort || 6565;
168
+ if (!healthPort || healthPort === 0) {
169
+ this.logger.debug('Health check server skipped (no healthPort set)');
170
+ return;
171
+ }
172
+ this.healthCheckServer = (0, node_http_1.createServer)((req, res) => {
173
+ if (req.url === '/health' && req.method === 'GET') {
174
+ if (this.isReady) {
175
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
176
+ res.end('responsive');
177
+ }
178
+ else {
179
+ res.writeHead(503, { 'Content-Type': 'text/plain' });
180
+ res.end('Service Unavailable');
181
+ }
182
+ }
183
+ else {
184
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
185
+ res.end('Not Found');
186
+ }
187
+ });
188
+ return new Promise((resolve, reject) => {
189
+ this.healthCheckServer.listen(healthPort, '0.0.0.0', () => {
190
+ this.logger.info({ port: healthPort }, 'Health check server started on port');
191
+ resolve();
192
+ });
193
+ this.healthCheckServer.on('error', (err) => {
194
+ this.logger.error({ err, port: healthPort }, 'Failed to start health check server');
195
+ reject(err);
196
+ });
197
+ });
198
+ }
199
+ async stopHealthCheckServer() {
200
+ if (!this.healthCheckServer) {
201
+ return;
202
+ }
203
+ return new Promise((resolve) => {
204
+ this.healthCheckServer.close(() => {
205
+ this.logger.info('Health check server stopped');
206
+ resolve();
207
+ });
208
+ });
209
+ }
210
+ async registerService() {
211
+ var _a, _b;
212
+ const primary = (_a = this.services[0]) === null || _a === void 0 ? void 0 : _a.definition;
213
+ if (!primary) {
214
+ throw new Error('Unexpected missing service definition during registration.');
215
+ }
216
+ const options = (_b = (0, decorators_js_1.getServiceOptions)(primary.ctor)) !== null && _b !== void 0 ? _b : {};
217
+ const isDeployed = isRunningInContainer();
218
+ const request = {
219
+ type: 'basic',
220
+ name: primary.qualifiedName,
221
+ beamoName: primary.name,
222
+ };
223
+ if (!isDeployed && this.env.routingKey) {
224
+ request.routingKey = this.env.routingKey;
225
+ }
226
+ if (!isDeployed && this.env.accountId) {
227
+ request.startedById = this.env.accountId;
228
+ }
229
+ const serializedRequest = JSON.stringify(request);
230
+ this.logger.debug({
231
+ request: {
232
+ type: request.type,
233
+ name: request.name,
234
+ beamoName: request.beamoName,
235
+ routingKey: request.routingKey,
236
+ startedById: request.startedById,
237
+ },
238
+ serializedJson: serializedRequest,
239
+ isDeployed,
240
+ }, 'Registering service provider with gateway.');
241
+ try {
242
+ await this.requester.request('post', 'gateway/provider', request);
243
+ this.logger.info({ serviceName: primary.qualifiedName }, 'Service provider registered successfully.');
244
+ if (isDeployed) {
245
+ this.logger.debug('Waiting for gateway to establish bindings after registration...');
246
+ await new Promise((resolve) => setTimeout(resolve, 2000));
247
+ this.logger.debug('Wait complete, gateway should have established bindings by now.');
248
+ }
249
+ }
250
+ catch (error) {
251
+ this.logger.error({
252
+ err: error,
253
+ request,
254
+ serviceName: primary.qualifiedName,
255
+ errorMessage: error instanceof Error ? error.message : String(error),
256
+ }, 'Failed to register service provider with gateway. This will prevent the service from receiving requests.');
257
+ throw error;
258
+ }
259
+ if (options.disableAllBeamableEvents) {
260
+ this.logger.info('Beamable events disabled by configuration.');
261
+ }
262
+ else {
263
+ const eventRequest = {
264
+ type: 'event',
265
+ evtWhitelist: ['content.manifest', 'realm.config', 'logging.context'],
266
+ };
267
+ try {
268
+ await this.requester.request('post', 'gateway/provider', eventRequest);
269
+ }
270
+ catch (error) {
271
+ this.logger.warn({ err: error }, 'Failed to register event provider. Continuing without events.');
272
+ }
273
+ }
274
+ }
275
+ async handleEvent(envelope) {
276
+ try {
277
+ if (!envelope.path) {
278
+ this.logger.debug({ envelope }, 'Ignoring websocket event without path.');
279
+ return;
280
+ }
281
+ if (envelope.path.startsWith('event/')) {
282
+ await this.requester.acknowledge(envelope.id);
283
+ return;
284
+ }
285
+ const context = this.toRequestContext(envelope);
286
+ await this.dispatch(context);
287
+ }
288
+ catch (error) {
289
+ const err = error instanceof Error ? error : new Error(String(error));
290
+ this.logger.error({ err, envelope }, 'Failed to handle websocket event.');
291
+ const status = this.resolveErrorStatus(err);
292
+ const response = {
293
+ id: envelope.id,
294
+ status,
295
+ body: {
296
+ error: err.name,
297
+ message: err.message,
298
+ },
299
+ };
300
+ await this.requester.sendResponse(response);
301
+ }
302
+ }
303
+ toRequestContext(envelope) {
304
+ var _a, _b, _c, _d, _e;
305
+ const path = (_a = envelope.path) !== null && _a !== void 0 ? _a : '';
306
+ const method = (_b = envelope.method) !== null && _b !== void 0 ? _b : 'post';
307
+ const userId = typeof envelope.from === 'number' ? envelope.from : 0;
308
+ const headers = (_c = envelope.headers) !== null && _c !== void 0 ? _c : {};
309
+ const [pathWithoutQuery, queryString] = path.split('?', 2);
310
+ const query = this.parseQueryString(queryString !== null && queryString !== void 0 ? queryString : '');
311
+ const envelopeScopes = (_d = envelope.scopes) !== null && _d !== void 0 ? _d : [];
312
+ let scopes = normalizeScopes(envelopeScopes);
313
+ const pathLower = pathWithoutQuery.toLowerCase();
314
+ if (pathLower.includes('/admin/') && scopes.size === 0) {
315
+ scopes = normalizeScopes(['admin']);
316
+ }
317
+ let body;
318
+ if (envelope.body && typeof envelope.body === 'string') {
319
+ try {
320
+ body = JSON.parse(envelope.body);
321
+ }
322
+ catch (error) {
323
+ this.logger.warn({ err: error, raw: envelope.body }, 'Failed to parse body string.');
324
+ }
325
+ }
326
+ else if (envelope.body && typeof envelope.body === 'object') {
327
+ body = envelope.body;
328
+ }
329
+ let payload;
330
+ if (body && typeof body === 'object' && 'payload' in body) {
331
+ const rawPayload = body.payload;
332
+ if (typeof rawPayload === 'string') {
333
+ try {
334
+ payload = JSON.parse(rawPayload);
335
+ }
336
+ catch (error) {
337
+ this.logger.warn({ err: error }, 'Failed to parse payload JSON.');
338
+ payload = rawPayload;
339
+ }
340
+ }
341
+ else {
342
+ payload = rawPayload;
343
+ }
344
+ }
345
+ const targetService = this.findServiceForPath(pathWithoutQuery);
346
+ const services = this.serviceManager.createFacade(userId, scopes, targetService === null || targetService === void 0 ? void 0 : targetService.definition.name);
347
+ const provider = this.createRequestScope(pathWithoutQuery, targetService);
348
+ const context = {
349
+ id: envelope.id,
350
+ path,
351
+ method,
352
+ status: (_e = envelope.status) !== null && _e !== void 0 ? _e : 0,
353
+ userId,
354
+ payload,
355
+ body,
356
+ query,
357
+ scopes,
358
+ headers,
359
+ cid: this.env.cid,
360
+ pid: this.env.pid,
361
+ services,
362
+ throwIfCancelled: () => { },
363
+ isCancelled: () => false,
364
+ hasScopes: (...requiredScopes) => requiredScopes.every((scope) => scopeSetHas(scopes, scope)),
365
+ requireScopes: (...requiredScopes) => {
366
+ const missingScopes = requiredScopes.filter((scope) => !scopeSetHas(scopes, scope));
367
+ if (missingScopes.length > 0) {
368
+ throw new errors_js_1.MissingScopesError(missingScopes);
369
+ }
370
+ },
371
+ provider,
372
+ };
373
+ provider.setInstance(dependency_js_1.REQUEST_CONTEXT_TOKEN, context);
374
+ provider.setInstance(dependency_js_1.BEAMABLE_SERVICES_TOKEN, services);
375
+ return context;
376
+ }
377
+ findServiceForPath(path) {
378
+ const pathLower = path.toLowerCase();
379
+ return this.services.find((service) => {
380
+ const qualifiedNameLower = service.definition.qualifiedName.toLowerCase();
381
+ return pathLower.startsWith(`${qualifiedNameLower}/`);
382
+ });
383
+ }
384
+ parseQueryString(queryString) {
385
+ if (!queryString || queryString.trim().length === 0) {
386
+ return {};
387
+ }
388
+ const params = {};
389
+ const pairs = queryString.split('&');
390
+ for (const pair of pairs) {
391
+ const [key, value = ''] = pair.split('=', 2);
392
+ if (key) {
393
+ const decodedKey = decodeURIComponent(key);
394
+ const decodedValue = decodeURIComponent(value);
395
+ if (decodedKey in params) {
396
+ const existing = params[decodedKey];
397
+ if (Array.isArray(existing)) {
398
+ existing.push(decodedValue);
399
+ }
400
+ else {
401
+ params[decodedKey] = [existing, decodedValue];
402
+ }
403
+ }
404
+ else {
405
+ params[decodedKey] = decodedValue;
406
+ }
407
+ }
408
+ }
409
+ return params;
410
+ }
411
+ async dispatch(ctx) {
412
+ const pathWithoutQuery = this.getPathWithoutQuery(ctx.path);
413
+ const service = this.findServiceForPath(pathWithoutQuery);
414
+ if (!service) {
415
+ throw new errors_js_1.UnknownRouteError(ctx.path);
416
+ }
417
+ if (await this.tryHandleFederationRoute(ctx, service)) {
418
+ return;
419
+ }
420
+ if (await this.tryHandleAdminRoute(ctx, service)) {
421
+ return;
422
+ }
423
+ const pathLower = pathWithoutQuery.toLowerCase();
424
+ const qualifiedNameLower = service.definition.qualifiedName.toLowerCase();
425
+ const route = pathLower.substring(qualifiedNameLower.length + 1);
426
+ const metadata = service.definition.callables.get(route);
427
+ if (!metadata) {
428
+ throw new errors_js_1.UnknownRouteError(ctx.path);
429
+ }
430
+ if (metadata.requireAuth && ctx.userId <= 0) {
431
+ if (metadata.access === 'server' && scopeSetHas(ctx.scopes, 'server')) {
432
+ }
433
+ else if (metadata.access === 'admin' && scopeSetHas(ctx.scopes, 'admin')) {
434
+ }
435
+ else {
436
+ throw new errors_js_1.UnauthorizedUserError(route);
437
+ }
438
+ }
439
+ enforceAccess(metadata.access, ctx);
440
+ if (metadata.requiredScopes.length > 0) {
441
+ const missing = metadata.requiredScopes.filter((scope) => !scopeSetHas(ctx.scopes, scope));
442
+ if (missing.length > 0) {
443
+ throw new errors_js_1.MissingScopesError(missing);
444
+ }
445
+ }
446
+ const handler = service.instance[metadata.displayName];
447
+ if (typeof handler !== 'function') {
448
+ throw new Error(`Callable ${metadata.displayName} is not a function on service ${service.definition.name}.`);
449
+ }
450
+ const args = this.buildInvocationArguments(handler, ctx);
451
+ const result = await Promise.resolve(handler.apply(service.instance, args));
452
+ await this.sendSuccessResponse(ctx, metadata, result);
453
+ }
454
+ buildInvocationArguments(handler, ctx) {
455
+ const payload = Array.isArray(ctx.payload)
456
+ ? ctx.payload
457
+ : typeof ctx.payload === 'string'
458
+ ? [ctx.payload]
459
+ : ctx.payload === undefined
460
+ ? []
461
+ : [ctx.payload];
462
+ const expectedParams = handler.length;
463
+ if (expectedParams === payload.length + 1) {
464
+ return [ctx, ...payload];
465
+ }
466
+ return payload;
467
+ }
468
+ async sendSuccessResponse(ctx, metadata, result) {
469
+ let body;
470
+ if (metadata.useLegacySerialization) {
471
+ const serialized = typeof result === 'string' ? result : JSON.stringify(result !== null && result !== void 0 ? result : null);
472
+ body = { payload: serialized };
473
+ }
474
+ else {
475
+ body = result !== null && result !== void 0 ? result : null;
476
+ }
477
+ const response = {
478
+ id: ctx.id,
479
+ status: 200,
480
+ body,
481
+ };
482
+ await this.requester.sendResponse(response);
483
+ }
484
+ async sendFederationResponse(ctx, result) {
485
+ const response = {
486
+ id: ctx.id,
487
+ status: 200,
488
+ body: result !== null && result !== void 0 ? result : null,
489
+ };
490
+ await this.requester.sendResponse(response);
491
+ }
492
+ async tryHandleFederationRoute(ctx, service) {
493
+ const pathWithoutQuery = this.getPathWithoutQuery(ctx.path);
494
+ const pathLower = pathWithoutQuery.toLowerCase();
495
+ const qualifiedNameLower = service.definition.qualifiedName.toLowerCase();
496
+ const prefixLower = `${qualifiedNameLower}/`;
497
+ if (!pathLower.startsWith(prefixLower)) {
498
+ return false;
499
+ }
500
+ const relativePath = pathWithoutQuery.substring(qualifiedNameLower.length + 1);
501
+ const handler = service.federationRegistry.resolve(relativePath);
502
+ if (!handler) {
503
+ return false;
504
+ }
505
+ const result = await handler.invoke(ctx);
506
+ await this.sendFederationResponse(ctx, result);
507
+ return true;
508
+ }
509
+ async tryHandleAdminRoute(ctx, service) {
510
+ var _a, _b;
511
+ const pathWithoutQuery = this.getPathWithoutQuery(ctx.path);
512
+ const pathLower = pathWithoutQuery.toLowerCase();
513
+ const adminPrefixLower = `${service.definition.qualifiedName.toLowerCase()}/admin/`;
514
+ if (!pathLower.startsWith(adminPrefixLower)) {
515
+ return false;
516
+ }
517
+ const options = (_a = (0, decorators_js_1.getServiceOptions)(service.definition.ctor)) !== null && _a !== void 0 ? _a : {};
518
+ const action = pathLower.substring(adminPrefixLower.length);
519
+ const requiresAdmin = action === 'metadata' || action === 'docs';
520
+ if (requiresAdmin && !scopeSetHas(ctx.scopes, 'admin')) {
521
+ const hasPortalHeader = ctx.headers['X-DE-SCOPE'] || ctx.headers['x-de-scope'];
522
+ if (hasPortalHeader) {
523
+ ctx.scopes.add('admin');
524
+ }
525
+ else {
526
+ throw new errors_js_1.MissingScopesError(['admin']);
527
+ }
528
+ }
529
+ switch (action) {
530
+ case 'health':
531
+ case 'healthcheck':
532
+ await this.requester.sendResponse({ id: ctx.id, status: 200, body: 'responsive' });
533
+ return true;
534
+ case 'metadata':
535
+ case 'docs':
536
+ break;
537
+ default:
538
+ if (!scopeSetHas(ctx.scopes, 'admin')) {
539
+ throw new errors_js_1.MissingScopesError(['admin']);
540
+ }
541
+ throw new errors_js_1.UnknownRouteError(ctx.path);
542
+ }
543
+ if (action === 'metadata') {
544
+ const federatedComponents = service.federationRegistry
545
+ .list()
546
+ .map((component) => ({
547
+ federationNamespace: component.federationNamespace,
548
+ federationType: component.federationType,
549
+ }));
550
+ await this.requester.sendResponse({
551
+ id: ctx.id,
552
+ status: 200,
553
+ body: {
554
+ serviceName: service.definition.name,
555
+ sdkVersion: this.env.sdkVersionExecution,
556
+ sdkBaseBuildVersion: this.env.sdkVersionExecution,
557
+ sdkExecutionVersion: this.env.sdkVersionExecution,
558
+ useLegacySerialization: Boolean(options.useLegacySerialization),
559
+ disableAllBeamableEvents: Boolean(options.disableAllBeamableEvents),
560
+ enableEagerContentLoading: false,
561
+ instanceId: this.microServiceId,
562
+ routingKey: (_b = this.env.routingKey) !== null && _b !== void 0 ? _b : '',
563
+ federatedComponents,
564
+ },
565
+ });
566
+ return true;
567
+ }
568
+ if (action === 'docs') {
569
+ try {
570
+ const document = (0, docs_js_1.generateOpenApiDocument)({
571
+ qualifiedName: service.definition.qualifiedName,
572
+ name: service.definition.name,
573
+ callables: Array.from(service.definition.callables.values()).map((callable) => ({
574
+ name: callable.displayName,
575
+ route: callable.route,
576
+ metadata: callable,
577
+ handler: typeof service.instance[callable.displayName] === 'function'
578
+ ? service.instance[callable.displayName]
579
+ : undefined,
580
+ })),
581
+ }, this.env, index_js_1.VERSION);
582
+ if (!document || Object.keys(document).length === 0) {
583
+ this.logger.warn({ serviceName: service.definition.name }, 'Generated OpenAPI document is empty');
584
+ }
585
+ await this.requester.sendResponse({
586
+ id: ctx.id,
587
+ status: 200,
588
+ body: document,
589
+ });
590
+ return true;
591
+ }
592
+ catch (error) {
593
+ this.logger.error({ err: error, serviceName: service.definition.name }, 'Failed to generate or send docs');
594
+ throw error;
595
+ }
596
+ }
597
+ return false;
598
+ }
599
+ resolveErrorStatus(error) {
600
+ if (error instanceof errors_js_1.UnauthorizedUserError) {
601
+ return 401;
602
+ }
603
+ if (error instanceof errors_js_1.MissingScopesError) {
604
+ return 403;
605
+ }
606
+ if (error instanceof errors_js_1.UnknownRouteError) {
607
+ return 404;
608
+ }
609
+ if (error instanceof errors_js_1.BeamableRuntimeError) {
610
+ return 500;
611
+ }
612
+ return 500;
613
+ }
614
+ printHelpfulUrls(service) {
615
+ if (!service) {
616
+ return;
617
+ }
618
+ if (process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true') {
619
+ return;
620
+ }
621
+ const docsUrl = buildDocsPortalUrl(this.env, service.definition);
622
+ const endpointBase = buildPostmanBaseUrl(this.env, service.definition);
623
+ const green = (text) => `\x1b[32m${text}\x1b[0m`;
624
+ const yellow = (text) => `\x1b[33m${text}\x1b[0m`;
625
+ const bannerLines = [
626
+ '',
627
+ yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'),
628
+ ` ${green('Beamable Node microservice ready:')} ${green(service.definition.name)}`,
629
+ yellow(' Quick shortcuts'),
630
+ ` ${yellow('Docs:')} ${docsUrl}`,
631
+ ` ${yellow('Endpoint:')} ${endpointBase}`,
632
+ yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'),
633
+ '',
634
+ ];
635
+ process.stdout.write(`${bannerLines.join('\n')}`);
636
+ }
637
+ async initializeDependencyProviders() {
638
+ for (const service of this.services) {
639
+ const builder = new dependency_js_1.DependencyBuilder();
640
+ builder.tryAddSingletonInstance(dependency_js_1.LOGGER_TOKEN, service.logger);
641
+ builder.tryAddSingletonInstance(dependency_js_1.ENVIRONMENT_CONFIG_TOKEN, this.env);
642
+ builder.tryAddSingletonInstance(services_js_1.BeamableServiceManager, this.serviceManager);
643
+ builder.tryAddSingletonInstance(service.definition.ctor, service.instance);
644
+ builder.tryAddSingletonInstance(federation_js_1.FederationRegistry, service.federationRegistry);
645
+ for (const handler of service.configureHandlers) {
646
+ await handler(builder);
647
+ }
648
+ const provider = builder.build();
649
+ service.provider = provider;
650
+ for (const handler of service.initializeHandlers) {
651
+ await handler(provider);
652
+ }
653
+ Object.defineProperty(service.instance, 'provider', {
654
+ value: provider,
655
+ enumerable: false,
656
+ configurable: false,
657
+ writable: false,
658
+ });
659
+ }
660
+ }
661
+ createRequestScope(path, service) {
662
+ var _a;
663
+ const targetService = service !== null && service !== void 0 ? service : this.findServiceForPath(path);
664
+ const provider = (_a = targetService === null || targetService === void 0 ? void 0 : targetService.provider) !== null && _a !== void 0 ? _a : new dependency_js_1.DependencyBuilder().build();
665
+ return provider.createScope();
666
+ }
667
+ getPathWithoutQuery(path) {
668
+ const [pathWithoutQuery] = path.split('?', 2);
669
+ return pathWithoutQuery;
670
+ }
671
+ }
672
+ exports.MicroserviceRuntime = MicroserviceRuntime;
673
+ function enforceAccess(access, ctx) {
674
+ switch (access) {
675
+ case 'client':
676
+ if (ctx.userId <= 0) {
677
+ throw new errors_js_1.UnauthorizedUserError(ctx.path);
678
+ }
679
+ break;
680
+ case 'server':
681
+ if (!scopeSetHas(ctx.scopes, 'server')) {
682
+ throw new errors_js_1.MissingScopesError(['server']);
683
+ }
684
+ break;
685
+ case 'admin':
686
+ if (!scopeSetHas(ctx.scopes, 'admin')) {
687
+ throw new errors_js_1.MissingScopesError(['admin']);
688
+ }
689
+ break;
690
+ default:
691
+ break;
692
+ }
693
+ }
694
+ function debugLog(...args) {
695
+ if (process.env.IS_LOCAL === '1' || process.env.IS_LOCAL === 'true') {
696
+ console.error(...args);
697
+ }
698
+ }
699
+ function isRunningInContainer() {
700
+ try {
701
+ const fs = require('fs');
702
+ if (fs.existsSync('/.dockerenv')) {
703
+ return true;
704
+ }
705
+ }
706
+ catch {
707
+ }
708
+ const hostname = process.env.HOSTNAME || '';
709
+ if (hostname && /^[a-f0-9]{12}$/i.test(hostname)) {
710
+ return true;
711
+ }
712
+ if (process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||
713
+ process.env.CONTAINER === 'beamable' ||
714
+ !!process.env.ECS_CONTAINER_METADATA_URI ||
715
+ !!process.env.KUBERNETES_SERVICE_HOST) {
716
+ return true;
717
+ }
718
+ return false;
719
+ }
720
+ function normalizeScopes(scopes) {
721
+ const normalized = new Set();
722
+ for (const scope of scopes) {
723
+ if (typeof scope !== 'string' || scope.trim().length === 0) {
724
+ continue;
725
+ }
726
+ normalized.add(scope.trim().toLowerCase());
727
+ }
728
+ return normalized;
729
+ }
730
+ function scopeSetHas(scopes, scope) {
731
+ const normalized = scope.trim().toLowerCase();
732
+ if (normalized.length === 0) {
733
+ return false;
734
+ }
735
+ return scopes.has('*') || scopes.has(normalized);
736
+ }
737
+ function buildPostmanBaseUrl(env, service) {
738
+ const httpHost = (0, urls_js_1.hostToHttpUrl)(env.host);
739
+ const routingKeyPart = env.routingKey ? env.routingKey : '';
740
+ return `${httpHost}/basic/${env.cid}.${env.pid}.${routingKeyPart}${service.qualifiedName}/`;
741
+ }
742
+ function buildDocsPortalUrl(env, service) {
743
+ const portalHost = (0, urls_js_1.hostToPortalUrl)((0, urls_js_1.hostToHttpUrl)(env.host));
744
+ const queryParams = { srcTool: 'node-runtime' };
745
+ if (env.routingKey) {
746
+ queryParams.routingKey = env.routingKey;
747
+ }
748
+ const query = new URLSearchParams(queryParams);
749
+ if (env.refreshToken) {
750
+ query.set('refresh_token', env.refreshToken);
751
+ }
752
+ const beamoId = service.name;
753
+ return `${portalHost}/${env.cid}/games/${env.pid}/realms/${env.pid}/microservices/micro_${beamoId}/docs?${query.toString()}`;
754
+ }
755
+ async function runMicroservice() {
756
+ debugLog('[BEAMABLE-NODE] Starting microservice...');
757
+ debugLog(`[BEAMABLE-NODE] Node version: ${process.version}`);
758
+ debugLog(`[BEAMABLE-NODE] Working directory: ${process.cwd()}`);
759
+ debugLog(`[BEAMABLE-NODE] Environment: ${JSON.stringify({
760
+ NODE_ENV: process.env.NODE_ENV,
761
+ CID: process.env.CID ? 'SET' : 'NOT SET',
762
+ PID: process.env.PID ? 'SET' : 'NOT SET',
763
+ HOST: process.env.HOST ? 'SET' : 'NOT SET',
764
+ SECRET: process.env.SECRET ? 'SET' : 'NOT SET',
765
+ })}`);
766
+ if (process.env.BEAMABLE_SKIP_RUNTIME === 'true') {
767
+ return;
768
+ }
769
+ const runtime = new MicroserviceRuntime();
770
+ process.on('uncaughtException', (error) => {
771
+ debugLog('Uncaught Exception:', error);
772
+ });
773
+ process.on('unhandledRejection', (reason, promise) => {
774
+ debugLog('Unhandled Rejection at:', promise, 'reason:', reason);
775
+ });
776
+ try {
777
+ await runtime.start();
778
+ }
779
+ catch (error) {
780
+ debugLog('Failed to start microservice runtime:', error);
781
+ }
782
+ const shutdownSignals = ['SIGTERM', 'SIGINT'];
783
+ shutdownSignals.forEach((signal) => {
784
+ process.once(signal, async () => {
785
+ await runtime.shutdown();
786
+ process.exit(0);
787
+ });
788
+ });
789
+ }
790
+ function generateOpenApiDocumentForRegisteredServices(overrides = {}, options = {}) {
791
+ const services = (0, decorators_js_1.listRegisteredServices)();
792
+ if (services.length === 0) {
793
+ throw new Error('No microservices registered. Import your service module before generating documentation.');
794
+ }
795
+ const baseEnv = buildEnvironmentConfig(overrides);
796
+ const primary = options.serviceName
797
+ ? services.find((svc) => svc.name === options.serviceName || svc.qualifiedName === options.serviceName)
798
+ : services[0];
799
+ if (!primary) {
800
+ throw new Error(`No registered microservice matched '${options.serviceName}'.`);
801
+ }
802
+ return (0, docs_js_1.generateOpenApiDocument)({
803
+ qualifiedName: primary.qualifiedName,
804
+ name: primary.name,
805
+ callables: Array.from(primary.callables.entries()).map(([displayName, metadata]) => ({
806
+ name: displayName,
807
+ route: metadata.route,
808
+ metadata,
809
+ handler: undefined,
810
+ })),
811
+ }, baseEnv, index_js_1.VERSION);
812
+ }
813
+ function buildEnvironmentConfig(overrides) {
814
+ var _a;
815
+ const merged = { ...overrides };
816
+ const ensure = (key, fallbackEnv) => {
817
+ if (merged[key] !== undefined) {
818
+ return;
819
+ }
820
+ if (fallbackEnv && process.env[fallbackEnv]) {
821
+ merged[key] = process.env[fallbackEnv];
822
+ }
823
+ };
824
+ ensure('cid', 'CID');
825
+ ensure('pid', 'PID');
826
+ ensure('host', 'HOST');
827
+ ensure('secret', 'SECRET');
828
+ ensure('refreshToken', 'REFRESH_TOKEN');
829
+ if (!merged.routingKey && !isRunningInContainer()) {
830
+ ensure('routingKey', 'NAME_PREFIX');
831
+ }
832
+ if (!merged.cid || !merged.pid || !merged.host) {
833
+ throw new Error('CID, PID, and HOST are required to generate documentation.');
834
+ }
835
+ const base = (0, env_js_1.loadEnvironmentConfig)();
836
+ return {
837
+ ...base,
838
+ ...merged,
839
+ routingKey: (_a = merged.routingKey) !== null && _a !== void 0 ? _a : base.routingKey,
840
+ };
841
+ }