@omen.foundation/node-microservice-runtime 0.1.65 → 0.1.67

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/src/docs.ts DELETED
@@ -1,262 +0,0 @@
1
- import type { EnvironmentConfig, ServiceCallableMetadata } from './types.js';
2
- import { hostToHttpUrl } from './utils/urls.js';
3
-
4
- const JSON_CONTENT_TYPE = 'application/json';
5
- const ADMIN_TAG = 'Administration';
6
-
7
- interface ServiceCallableDescriptor {
8
- name: string;
9
- route: string;
10
- metadata: ServiceCallableMetadata;
11
- handler?: (...args: unknown[]) => unknown;
12
- }
13
-
14
- interface ServiceDescriptorLite {
15
- qualifiedName: string;
16
- name: string;
17
- callables: ServiceCallableDescriptor[];
18
- }
19
-
20
- const SCOPE_SECURITY_SCHEME = {
21
- type: 'apiKey',
22
- name: 'X-DE-SCOPE',
23
- in: 'header',
24
- description: "Customer and project scope. This should contain the '<customer-id>.<project-id>'. Required for all microservice calls.",
25
- };
26
-
27
- const BEAM_SCOPE_SECURITY_SCHEME = {
28
- type: 'apiKey',
29
- name: 'X-BEAM-SCOPE',
30
- in: 'header',
31
- description: "Customer and project scope. This should contain the '<customer-id>.<project-id>'. Required for all microservice calls. Alternative to X-DE-SCOPE.",
32
- };
33
-
34
- const USER_SECURITY_SCHEME = {
35
- type: 'http',
36
- scheme: 'bearer',
37
- bearerFormat: 'Bearer <Access Token>',
38
- description: 'Bearer authentication with a player access token in the Authorization header.',
39
- };
40
-
41
- interface OpenApiDocument {
42
- openapi: string;
43
- info: {
44
- title: string;
45
- version: string;
46
- };
47
- servers: Array<{ url: string }>;
48
- paths: Record<string, unknown>;
49
- components: {
50
- securitySchemes: Record<string, unknown>;
51
- schemas: Record<string, unknown>;
52
- };
53
- }
54
-
55
- export function generateOpenApiDocument(service: ServiceDescriptorLite, env: EnvironmentConfig, runtimeVersion?: string): OpenApiDocument {
56
- const serverBase = buildServiceBaseUrl(env, service);
57
-
58
- const doc: OpenApiDocument = {
59
- openapi: '3.0.1',
60
- info: {
61
- title: service.name,
62
- version: runtimeVersion || process.env.BEAMABLE_RUNTIME_VERSION || '0.0.0',
63
- },
64
- servers: serverBase ? [{ url: serverBase }] : [],
65
- paths: {},
66
- components: {
67
- securitySchemes: {
68
- scope: SCOPE_SECURITY_SCHEME,
69
- beamScope: BEAM_SCOPE_SECURITY_SCHEME,
70
- user: USER_SECURITY_SCHEME,
71
- },
72
- schemas: {},
73
- },
74
- };
75
-
76
- for (const callable of service.callables) {
77
- const normalizedRoute = normalizeRoute(callable.route);
78
- const pathKey = `/${normalizedRoute}`;
79
- const payload = buildRequestSchema(callable.handler);
80
- // All endpoints require X-DE-SCOPE or X-BEAM-SCOPE for routing
81
- const securityEntry: Record<string, string[]> = { scope: [], beamScope: [] };
82
- if (callable.metadata.requireAuth) {
83
- securityEntry.user = [];
84
- }
85
-
86
- const operation: Record<string, unknown> = {
87
- operationId: callable.name,
88
- summary: callable.name,
89
- tags: callable.metadata.tags.length > 0 ? callable.metadata.tags : ['Callables'],
90
- responses: {
91
- '200': {
92
- description: 'Success',
93
- content: {
94
- [JSON_CONTENT_TYPE]: {
95
- schema: {
96
- oneOf: [{ type: 'object' }, { type: 'array' }, { type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'null' }],
97
- },
98
- },
99
- },
100
- },
101
- },
102
- security: [securityEntry],
103
- 'x-beamable-access': callable.metadata.access,
104
- };
105
-
106
- if (callable.metadata.requiredScopes.length > 0) {
107
- operation['x-beamable-required-scopes'] = callable.metadata.requiredScopes;
108
- }
109
-
110
- if (payload) {
111
- operation.requestBody = {
112
- required: payload.required,
113
- content: {
114
- [JSON_CONTENT_TYPE]: {
115
- schema: payload.schema,
116
- },
117
- },
118
- };
119
- }
120
-
121
- doc.paths[pathKey] = {
122
- post: operation,
123
- };
124
- }
125
-
126
- addAdminOperations(doc);
127
-
128
- return JSON.parse(JSON.stringify(doc)) as OpenApiDocument;
129
- }
130
-
131
- function buildRequestSchema(handler?: (...args: unknown[]) => unknown):
132
- | { required: boolean; schema: Record<string, unknown> }
133
- | undefined {
134
- if (typeof handler !== 'function') {
135
- return {
136
- required: false,
137
- schema: {
138
- type: 'object',
139
- properties: {
140
- payload: {
141
- type: 'array',
142
- description: 'Callable arguments in invocation order.',
143
- },
144
- },
145
- additionalProperties: false,
146
- },
147
- };
148
- }
149
-
150
- const expectedParams = handler.length;
151
- // The runtime injects RequestContext as the first argument when the handler expects one more parameter than provided.
152
- const payloadCount = Math.max(expectedParams - 1, 0);
153
-
154
- if (payloadCount <= 0) {
155
- return undefined;
156
- }
157
-
158
- return {
159
- required: true,
160
- schema: {
161
- type: 'object',
162
- properties: {
163
- payload: {
164
- type: 'array',
165
- minItems: payloadCount,
166
- description: 'Callable arguments in invocation order.',
167
- },
168
- },
169
- required: ['payload'],
170
- additionalProperties: false,
171
- },
172
- };
173
- }
174
-
175
- function buildServiceBaseUrl(env: EnvironmentConfig, service: ServiceDescriptorLite): string {
176
- const host = hostToHttpUrl(env.host).replace(/\/$/, '');
177
- return `${host}/basic/${env.cid}.${env.pid}.${service.qualifiedName}/`;
178
- }
179
-
180
- function normalizeRoute(route: string): string {
181
- if (!route) {
182
- return '';
183
- }
184
- return route.replace(/^\/+/, '');
185
- }
186
-
187
- function addAdminOperations(doc: OpenApiDocument): void {
188
- // All endpoints require X-DE-SCOPE or X-BEAM-SCOPE for routing
189
- const requiredScopeSecurity: Record<string, string[]> = { scope: [], beamScope: [] };
190
-
191
- doc.paths['/admin/HealthCheck'] = {
192
- post: {
193
- operationId: 'adminHealthCheck',
194
- summary: 'HealthCheck',
195
- tags: [ADMIN_TAG],
196
- responses: {
197
- '200': {
198
- description: 'The word "responsive" if all is well.',
199
- content: {
200
- [JSON_CONTENT_TYPE]: {
201
- schema: {
202
- type: 'string',
203
- example: 'responsive',
204
- },
205
- },
206
- },
207
- },
208
- },
209
- security: [requiredScopeSecurity],
210
- // Note: The portal constructs X-BEAM-SERVICE-ROUTING-KEY header from the service name
211
- // in the server URL. The service name format must match what we registered with.
212
- },
213
- };
214
-
215
- const adminSecurity: Record<string, string[]> = { scope: ['admin'], beamScope: ['admin'] };
216
-
217
- doc.paths['/admin/Docs'] = {
218
- post: {
219
- operationId: 'adminDocs',
220
- summary: 'Docs',
221
- description: 'Generates an OpenAPI/Swagger 3.0 document that describes the available service endpoints.',
222
- tags: [ADMIN_TAG],
223
- responses: {
224
- '200': {
225
- description: 'Swagger JSON document.',
226
- content: {
227
- [JSON_CONTENT_TYPE]: {
228
- schema: {
229
- type: 'object',
230
- },
231
- },
232
- },
233
- },
234
- },
235
- security: [adminSecurity],
236
- 'x-beamable-required-scopes': ['admin'],
237
- },
238
- };
239
-
240
- doc.paths['/admin/Metadata'] = {
241
- post: {
242
- operationId: 'adminMetadata',
243
- summary: 'Metadata',
244
- description: 'Fetch various Beamable SDK metadata for the Microservice.',
245
- tags: [ADMIN_TAG],
246
- responses: {
247
- '200': {
248
- description: 'Service metadata.',
249
- content: {
250
- [JSON_CONTENT_TYPE]: {
251
- schema: {
252
- type: 'object',
253
- },
254
- },
255
- },
256
- },
257
- },
258
- security: [adminSecurity],
259
- 'x-beamable-required-scopes': ['admin'],
260
- },
261
- };
262
- }
package/src/env.ts DELETED
@@ -1,148 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import type { EnvironmentConfig } from './types.js';
5
- import { getDefaultRoutingKeyForMachine } from './routing.js';
6
-
7
- function getBoolean(name: string, defaultValue = false): boolean {
8
- const raw = process.env[name];
9
- if (!raw) {
10
- return defaultValue;
11
- }
12
- return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
13
- }
14
-
15
- function getNumber(name: string, defaultValue: number): number {
16
- const raw = process.env[name];
17
- if (!raw) {
18
- return defaultValue;
19
- }
20
- const parsed = Number(raw);
21
- return Number.isFinite(parsed) ? parsed : defaultValue;
22
- }
23
-
24
- function resolveHealthPort(): number {
25
- // Always default to 6565 if HEALTH_PORT is not explicitly set
26
- // This ensures the health check server starts in deployed environments
27
- // even if container detection fails or HEALTH_PORT env var is missing
28
- const preferred = getNumber('HEALTH_PORT', 6565);
29
- if (preferred > 0) {
30
- return preferred;
31
- }
32
- // Use PID-scoped pseudo random port to avoid collisions on dev machines.
33
- const base = 45000;
34
- const span = 2000;
35
- const candidate = base + (process.pid % span);
36
- return candidate;
37
- }
38
-
39
- function isInContainer(): boolean {
40
- // Check for Docker container
41
- try {
42
- const fs = require('fs');
43
- if (fs.existsSync('/.dockerenv')) {
44
- return true;
45
- }
46
- } catch {
47
- // fs might not be available
48
- }
49
-
50
- // Check for Docker container hostname pattern (12 hex chars)
51
- const hostname = process.env.HOSTNAME || '';
52
- if (hostname && /^[a-f0-9]{12}$/i.test(hostname)) {
53
- return true;
54
- }
55
-
56
- // Explicit container indicators
57
- if (
58
- process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||
59
- process.env.CONTAINER === 'beamable' ||
60
- !!process.env.ECS_CONTAINER_METADATA_URI ||
61
- !!process.env.KUBERNETES_SERVICE_HOST
62
- ) {
63
- return true;
64
- }
65
-
66
- return false;
67
- }
68
-
69
- function resolveRoutingKey(): string | undefined {
70
- const raw = process.env.NAME_PREFIX ?? process.env.ROUTING_KEY;
71
- if (raw && raw.trim().length > 0) {
72
- return raw.trim();
73
- }
74
-
75
- // If we're actually in a container, return undefined (deployed service)
76
- if (isInContainer()) {
77
- return undefined;
78
- }
79
-
80
- // Local development - always try to get a routing key
81
- try {
82
- return getDefaultRoutingKeyForMachine();
83
- } catch (error) {
84
- throw new Error(
85
- `Unable to determine routing key automatically. Set NAME_PREFIX environment variable. ${(error as Error).message}`,
86
- );
87
- }
88
- }
89
-
90
- function resolveSdkVersionExecution(): string {
91
- return process.env.BEAMABLE_SDK_VERSION_EXECUTION ?? '';
92
- }
93
-
94
- function resolveLogLevel(): string {
95
- const candidate = process.env.LOG_LEVEL ?? 'info';
96
- const allowed = new Set(['fatal', 'error', 'warn', 'info', 'debug', 'trace']);
97
- return allowed.has(candidate.toLowerCase()) ? candidate.toLowerCase() : 'info';
98
- }
99
-
100
- function resolveWatchToken(): boolean {
101
- const value = process.env.WATCH_TOKEN;
102
- if (value === undefined) {
103
- return false;
104
- }
105
- return getBoolean('WATCH_TOKEN', false);
106
- }
107
-
108
- function resolveBeamInstanceCount(): number {
109
- return getNumber('BEAM_INSTANCE_COUNT', 1);
110
- }
111
-
112
- export function loadEnvironmentConfig(): EnvironmentConfig {
113
- const cid = process.env.CID ?? '';
114
- const pid = process.env.PID ?? '';
115
- const host = process.env.HOST ?? '';
116
-
117
- if (!cid || !pid || !host) {
118
- throw new Error('Missing required Beamable environment variables (CID, PID, HOST).');
119
- }
120
-
121
- const config: EnvironmentConfig = {
122
- cid,
123
- pid,
124
- host,
125
- secret: process.env.SECRET ?? undefined,
126
- refreshToken: process.env.REFRESH_TOKEN ?? undefined,
127
- routingKey: resolveRoutingKey(),
128
- accountId: getNumber('USER_ACCOUNT_ID', 0) || undefined,
129
- accountEmail: process.env.USER_EMAIL ?? undefined,
130
- logLevel: resolveLogLevel(),
131
- healthPort: resolveHealthPort(),
132
- disableCustomInitializationHooks: getBoolean('DISABLE_CUSTOM_INITIALIZATION_HOOKS', false),
133
- watchToken: resolveWatchToken(),
134
- sdkVersionExecution: resolveSdkVersionExecution(),
135
- beamInstanceCount: resolveBeamInstanceCount(),
136
- logTruncateLimit: getNumber('LOG_TRUNCATE_LIMIT', 1000),
137
- };
138
-
139
- return config;
140
- }
141
-
142
- export function ensureWritableTempDirectory(): string {
143
- const candidate = process.env.LOG_PATH ?? join(tmpdir(), 'beamable-node-runtime');
144
- if (!existsSync(candidate)) {
145
- return candidate;
146
- }
147
- return candidate;
148
- }
package/src/errors.ts DELETED
@@ -1,55 +0,0 @@
1
- export class BeamableRuntimeError extends Error {
2
- public readonly code: string;
3
-
4
- constructor(code: string, message: string, cause?: unknown) {
5
- super(message);
6
- this.code = code;
7
- if (cause !== undefined) {
8
- (this as { cause?: unknown }).cause = cause;
9
- }
10
- this.name = this.constructor.name;
11
- Error.captureStackTrace?.(this, this.constructor);
12
- }
13
- }
14
-
15
- export class AuthenticationError extends BeamableRuntimeError {
16
- constructor(message: string, cause?: unknown) {
17
- super('AUTHENTICATION_FAILED', message, cause);
18
- }
19
- }
20
-
21
- export class TimeoutError extends BeamableRuntimeError {
22
- constructor(message: string, cause?: unknown) {
23
- super('TIMEOUT', message, cause);
24
- }
25
- }
26
-
27
- export class MissingScopesError extends BeamableRuntimeError {
28
- constructor(requiredScopes: Iterable<string>) {
29
- super('MISSING_SCOPES', `Missing required scopes: ${Array.from(requiredScopes).join(', ')}`);
30
- }
31
- }
32
-
33
- export class UnauthorizedUserError extends BeamableRuntimeError {
34
- constructor(route: string) {
35
- super('UNAUTHORIZED_USER', `Route "${route}" requires an authenticated user.`);
36
- }
37
- }
38
-
39
- export class UnknownRouteError extends BeamableRuntimeError {
40
- constructor(route: string) {
41
- super('UNKNOWN_ROUTE', `No callable registered for route "${route}".`);
42
- }
43
- }
44
-
45
- export class SerializationError extends BeamableRuntimeError {
46
- constructor(message: string, cause?: unknown) {
47
- super('SERIALIZATION_ERROR', message, cause);
48
- }
49
- }
50
-
51
- export class InvalidConfigurationError extends BeamableRuntimeError {
52
- constructor(message: string, cause?: unknown) {
53
- super('INVALID_CONFIGURATION', message, cause);
54
- }
55
- }