@omen.foundation/node-microservice-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.env +13 -0
  2. package/dist/auth.cjs +97 -0
  3. package/dist/auth.d.ts +14 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +93 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/cli/index.d.ts +3 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +588 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/decorators.cjs +181 -0
  12. package/dist/decorators.d.ts +23 -0
  13. package/dist/decorators.d.ts.map +1 -0
  14. package/dist/decorators.js +155 -0
  15. package/dist/decorators.js.map +1 -0
  16. package/dist/dependency.cjs +165 -0
  17. package/dist/dependency.d.ts +56 -0
  18. package/dist/dependency.d.ts.map +1 -0
  19. package/dist/dependency.js +162 -0
  20. package/dist/dependency.js.map +1 -0
  21. package/dist/dev.cjs +34 -0
  22. package/dist/dev.d.ts +9 -0
  23. package/dist/dev.d.ts.map +1 -0
  24. package/dist/dev.js +32 -0
  25. package/dist/dev.js.map +1 -0
  26. package/dist/discovery.cjs +79 -0
  27. package/dist/discovery.d.ts +20 -0
  28. package/dist/discovery.d.ts.map +1 -0
  29. package/dist/discovery.js +75 -0
  30. package/dist/discovery.js.map +1 -0
  31. package/dist/docs.cjs +206 -0
  32. package/dist/docs.d.ts +30 -0
  33. package/dist/docs.d.ts.map +1 -0
  34. package/dist/docs.js +209 -0
  35. package/dist/docs.js.map +1 -0
  36. package/dist/env.cjs +106 -0
  37. package/dist/env.d.ts +4 -0
  38. package/dist/env.d.ts.map +1 -0
  39. package/dist/env.js +108 -0
  40. package/dist/env.js.map +1 -0
  41. package/dist/errors.cjs +58 -0
  42. package/dist/errors.d.ts +26 -0
  43. package/dist/errors.d.ts.map +1 -0
  44. package/dist/errors.js +48 -0
  45. package/dist/errors.js.map +1 -0
  46. package/dist/federation.cjs +356 -0
  47. package/dist/federation.d.ts +108 -0
  48. package/dist/federation.d.ts.map +1 -0
  49. package/dist/federation.js +341 -0
  50. package/dist/federation.js.map +1 -0
  51. package/dist/index.cjs +42 -0
  52. package/dist/index.d.ts +13 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +10 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/inventory.cjs +361 -0
  57. package/dist/inventory.d.ts +116 -0
  58. package/dist/inventory.d.ts.map +1 -0
  59. package/dist/inventory.js +351 -0
  60. package/dist/inventory.js.map +1 -0
  61. package/dist/logger.cjs +62 -0
  62. package/dist/logger.d.ts +9 -0
  63. package/dist/logger.d.ts.map +1 -0
  64. package/dist/logger.js +29 -0
  65. package/dist/logger.js.map +1 -0
  66. package/dist/message.cjs +19 -0
  67. package/dist/message.d.ts +5 -0
  68. package/dist/message.d.ts.map +1 -0
  69. package/dist/message.js +15 -0
  70. package/dist/message.js.map +1 -0
  71. package/dist/requester.cjs +100 -0
  72. package/dist/requester.d.ts +20 -0
  73. package/dist/requester.d.ts.map +1 -0
  74. package/dist/requester.js +99 -0
  75. package/dist/requester.js.map +1 -0
  76. package/dist/routing.cjs +39 -0
  77. package/dist/routing.d.ts +2 -0
  78. package/dist/routing.d.ts.map +1 -0
  79. package/dist/routing.js +36 -0
  80. package/dist/routing.js.map +1 -0
  81. package/dist/runtime.cjs +735 -0
  82. package/dist/runtime.d.ts +40 -0
  83. package/dist/runtime.d.ts.map +1 -0
  84. package/dist/runtime.js +825 -0
  85. package/dist/runtime.js.map +1 -0
  86. package/dist/services.cjs +346 -0
  87. package/dist/services.d.ts +46 -0
  88. package/dist/services.d.ts.map +1 -0
  89. package/dist/services.js +343 -0
  90. package/dist/services.js.map +1 -0
  91. package/dist/storage.cjs +147 -0
  92. package/dist/storage.d.ts +46 -0
  93. package/dist/storage.d.ts.map +1 -0
  94. package/dist/storage.js +144 -0
  95. package/dist/storage.js.map +1 -0
  96. package/dist/types.cjs +2 -0
  97. package/dist/types.d.ts +108 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +2 -0
  100. package/dist/types.js.map +1 -0
  101. package/dist/utils/urls.cjs +55 -0
  102. package/dist/utils/urls.d.ts +5 -0
  103. package/dist/utils/urls.d.ts.map +1 -0
  104. package/dist/utils/urls.js +50 -0
  105. package/dist/utils/urls.js.map +1 -0
  106. package/dist/websocket.cjs +142 -0
  107. package/dist/websocket.d.ts +33 -0
  108. package/dist/websocket.d.ts.map +1 -0
  109. package/dist/websocket.js +139 -0
  110. package/dist/websocket.js.map +1 -0
  111. package/env.sample +13 -0
  112. package/package.json +49 -0
  113. package/scripts/generate-openapi.mjs +114 -0
  114. package/scripts/lib/cli-utils.mjs +58 -0
  115. package/scripts/prepare-cjs.mjs +44 -0
  116. package/scripts/publish-service.mjs +1126 -0
  117. package/scripts/validate-service.mjs +103 -0
  118. package/scripts/ws-test.mjs +25 -0
  119. package/src/auth.ts +117 -0
  120. package/src/cli/index.ts +699 -0
  121. package/src/decorators.ts +207 -0
  122. package/src/dependency.ts +211 -0
  123. package/src/dev.ts +17 -0
  124. package/src/discovery.ts +88 -0
  125. package/src/docs.ts +262 -0
  126. package/src/env.ts +125 -0
  127. package/src/errors.ts +55 -0
  128. package/src/federation.ts +559 -0
  129. package/src/index.ts +51 -0
  130. package/src/inventory.ts +491 -0
  131. package/src/logger.ts +38 -0
  132. package/src/message.ts +19 -0
  133. package/src/requester.ts +126 -0
  134. package/src/routing.ts +42 -0
  135. package/src/runtime.ts +967 -0
  136. package/src/services.ts +459 -0
  137. package/src/storage.ts +206 -0
  138. package/src/types/beamable-sdk-api.d.ts +5 -0
  139. package/src/types.ts +117 -0
  140. package/src/utils/urls.ts +53 -0
  141. package/src/websocket.ts +170 -0
  142. package/tsconfig.base.json +31 -0
  143. package/tsconfig.build.json +10 -0
  144. package/tsconfig.cjs.json +16 -0
  145. package/tsconfig.dev.json +14 -0
@@ -0,0 +1,491 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Logger } from 'pino';
3
+ import type { BoundBeamApi } from './services.js';
4
+ import { MissingScopesError } from './errors.js';
5
+
6
+ export interface CurrencyProperty {
7
+ name: string;
8
+ value: string;
9
+ }
10
+
11
+ interface ItemProperty {
12
+ name: string;
13
+ value: string;
14
+ }
15
+
16
+ interface ItemCreateRequest {
17
+ contentId: string;
18
+ properties: ItemProperty[];
19
+ reqId?: string;
20
+ }
21
+
22
+ interface ItemDeleteRequest {
23
+ contentId: string;
24
+ id: string;
25
+ }
26
+
27
+ interface ItemUpdateRequest {
28
+ contentId: string;
29
+ id: string;
30
+ properties: ItemProperty[];
31
+ }
32
+
33
+ interface CurrencyView {
34
+ id: string;
35
+ amount: string | number | bigint;
36
+ }
37
+
38
+ export interface InventoryView {
39
+ currencies?: CurrencyView[];
40
+ }
41
+
42
+ export interface PreviewVipBonusResponse {
43
+ currencies: Array<{ id: string; amount: string | number | bigint; originalAmount?: string | number | bigint }>;
44
+ }
45
+
46
+ export interface MultipliersGetResponse {
47
+ [key: string]: unknown;
48
+ }
49
+
50
+ interface TransferRequestPayload {
51
+ recipientPlayer: string;
52
+ currencies?: Record<string, string>;
53
+ transaction?: string;
54
+ }
55
+
56
+ export interface InventoryUpdatePayload {
57
+ transaction?: string;
58
+ applyVipBonus?: boolean;
59
+ currencies?: Record<string, string>;
60
+ currencyProperties?: Record<string, CurrencyProperty[]>;
61
+ newItems?: Array<ItemCreateRequest>;
62
+ deleteItems?: Array<ItemDeleteRequest>;
63
+ updateItems?: Array<ItemUpdateRequest>;
64
+ }
65
+
66
+ export interface InventoryCurrencyChanges {
67
+ [currencyId: string]: bigint;
68
+ }
69
+
70
+ export interface InventoryBuilderOptions {
71
+ transaction?: string;
72
+ }
73
+
74
+ export class InventoryUpdateBuilder {
75
+ private readonly currencyChanges: Map<string, bigint> = new Map();
76
+ private readonly currencyPropertyUpdates: Map<string, CurrencyProperty[]> = new Map();
77
+ private readonly newItemRequests: Array<ItemCreateRequest & { reqId: string }> = [];
78
+ private readonly deleteItemRequests: ItemDeleteRequest[] = [];
79
+ private readonly updateItemRequests: ItemUpdateRequest[] = [];
80
+ private applyVipBonusFlag?: boolean;
81
+
82
+ applyVipBonus(apply: boolean): this {
83
+ this.applyVipBonusFlag = apply;
84
+ return this;
85
+ }
86
+
87
+ currencyChange(currencyId: string, delta: bigint | number): this {
88
+ const normalizedId = this.normalizeId(currencyId, 'currency');
89
+ const current = this.currencyChanges.get(normalizedId) ?? BigInt(0);
90
+ const next = current + BigInt(delta);
91
+ if (next === BigInt(0)) {
92
+ this.currencyChanges.delete(normalizedId);
93
+ } else {
94
+ this.currencyChanges.set(normalizedId, next);
95
+ }
96
+ return this;
97
+ }
98
+
99
+ setCurrencyProperties(currencyId: string, properties: CurrencyProperty[]): this {
100
+ const normalizedId = this.normalizeId(currencyId, 'currency');
101
+ this.currencyPropertyUpdates.set(normalizedId, [...properties]);
102
+ return this;
103
+ }
104
+
105
+ addItem(contentId: string, properties: Record<string, string> = {}, requestId: string = randomUUID()): this {
106
+ const normalizedId = this.normalizeId(contentId, 'item');
107
+ const itemProperties = Object.entries(properties).map<ItemProperty>(([name, value]) => ({
108
+ name,
109
+ value,
110
+ }));
111
+ this.newItemRequests.push({
112
+ contentId: normalizedId,
113
+ properties: itemProperties,
114
+ reqId: requestId,
115
+ });
116
+ return this;
117
+ }
118
+
119
+ deleteItem(contentId: string, itemId: bigint | number | string): this {
120
+ const normalizedId = this.normalizeId(contentId, 'item');
121
+ this.deleteItemRequests.push({
122
+ contentId: normalizedId,
123
+ id: this.toIdentifier(itemId),
124
+ });
125
+ return this;
126
+ }
127
+
128
+ updateItem(contentId: string, itemId: bigint | number | string, properties: Record<string, string>): this {
129
+ const normalizedId = this.normalizeId(contentId, 'item');
130
+ const normalizedProperties = Object.entries(properties).map<ItemProperty>(([name, value]) => ({
131
+ name,
132
+ value,
133
+ }));
134
+ this.updateItemRequests.push({
135
+ contentId: normalizedId,
136
+ id: this.toIdentifier(itemId),
137
+ properties: normalizedProperties,
138
+ });
139
+ return this;
140
+ }
141
+
142
+ merge(other: InventoryUpdateBuilder): this {
143
+ for (const [currencyId, delta] of other.currencyChanges.entries()) {
144
+ this.currencyChange(currencyId, delta);
145
+ }
146
+ for (const [currencyId, props] of other.currencyPropertyUpdates.entries()) {
147
+ this.currencyPropertyUpdates.set(currencyId, [...props]);
148
+ }
149
+ for (const item of other.newItemRequests) {
150
+ this.newItemRequests.push({ ...item });
151
+ }
152
+ for (const item of other.deleteItemRequests) {
153
+ this.deleteItemRequests.push({ ...item });
154
+ }
155
+ for (const item of other.updateItemRequests) {
156
+ this.updateItemRequests.push({
157
+ contentId: item.contentId,
158
+ id: this.toIdentifier(item.id),
159
+ properties: [...item.properties],
160
+ });
161
+ }
162
+ if (other.applyVipBonusFlag !== undefined) {
163
+ this.applyVipBonusFlag = other.applyVipBonusFlag;
164
+ }
165
+ return this;
166
+ }
167
+
168
+ isEmpty(): boolean {
169
+ return (
170
+ this.currencyChanges.size === 0 &&
171
+ this.currencyPropertyUpdates.size === 0 &&
172
+ this.newItemRequests.length === 0 &&
173
+ this.deleteItemRequests.length === 0 &&
174
+ this.updateItemRequests.length === 0 &&
175
+ this.applyVipBonusFlag === undefined
176
+ );
177
+ }
178
+
179
+ toRequest(options: InventoryBuilderOptions = {}): InventoryUpdatePayload {
180
+ const payload: InventoryUpdatePayload = {};
181
+
182
+ if (options.transaction) {
183
+ payload.transaction = options.transaction;
184
+ }
185
+ if (this.applyVipBonusFlag !== undefined) {
186
+ payload.applyVipBonus = this.applyVipBonusFlag;
187
+ }
188
+ if (this.currencyChanges.size > 0) {
189
+ const currencies: Record<string, string> = {};
190
+ for (const [currencyId, delta] of this.currencyChanges.entries()) {
191
+ currencies[currencyId] = delta.toString();
192
+ }
193
+ payload.currencies = currencies;
194
+ }
195
+ if (this.currencyPropertyUpdates.size > 0) {
196
+ const currencyProperties: Record<string, CurrencyProperty[]> = {};
197
+ for (const [currencyId, properties] of this.currencyPropertyUpdates.entries()) {
198
+ currencyProperties[currencyId] = properties.map<CurrencyProperty>((property) => ({ ...property }));
199
+ }
200
+ payload.currencyProperties = currencyProperties;
201
+ }
202
+ if (this.newItemRequests.length > 0) {
203
+ payload.newItems = this.newItemRequests.map((item) => ({
204
+ contentId: item.contentId,
205
+ properties: item.properties.map<ItemProperty>((property) => ({ ...property })),
206
+ reqId: item.reqId,
207
+ }));
208
+ }
209
+ if (this.deleteItemRequests.length > 0) {
210
+ payload.deleteItems = this.deleteItemRequests.map((item) => ({
211
+ contentId: item.contentId,
212
+ id: this.toIdentifier(item.id),
213
+ }));
214
+ }
215
+ if (this.updateItemRequests.length > 0) {
216
+ payload.updateItems = this.updateItemRequests.map((item) => ({
217
+ contentId: item.contentId,
218
+ id: this.toIdentifier(item.id),
219
+ properties: item.properties.map<ItemProperty>((property) => ({ ...property })),
220
+ }));
221
+ }
222
+
223
+ return payload;
224
+ }
225
+
226
+ private normalizeId(value: string, kind: 'currency' | 'item'): string {
227
+ const trimmed = value.trim();
228
+ if (!trimmed) {
229
+ throw new Error(`Invalid ${kind} identifier: "${value}"`);
230
+ }
231
+ return trimmed;
232
+ }
233
+
234
+ private toIdentifier(value: bigint | number | string): string {
235
+ if (typeof value === 'bigint') {
236
+ return value.toString();
237
+ }
238
+ if (typeof value === 'number') {
239
+ if (!Number.isFinite(value)) {
240
+ throw new Error(`Invalid numeric identifier: ${value}`);
241
+ }
242
+ return Math.trunc(value).toString();
243
+ }
244
+ const trimmed = value.trim();
245
+ if (!trimmed) {
246
+ throw new Error('Identifier cannot be empty.');
247
+ }
248
+ return trimmed;
249
+ }
250
+ }
251
+
252
+ interface InventoryServiceDependencies {
253
+ api: BoundBeamApi;
254
+ logger: Logger;
255
+ userId: string;
256
+ scopeChecker: (scopes: string[]) => void;
257
+ }
258
+
259
+ export class InventoryService {
260
+ private readonly api: BoundBeamApi;
261
+ private readonly logger: Logger;
262
+ private readonly userId: string;
263
+ private readonly ensureScopes: (scopes: string[]) => void;
264
+
265
+ constructor(dependencies: InventoryServiceDependencies) {
266
+ this.api = dependencies.api;
267
+ this.logger = dependencies.logger.child({ component: 'InventoryService' });
268
+ this.userId = dependencies.userId;
269
+ this.ensureScopes = dependencies.scopeChecker;
270
+ }
271
+
272
+ createBuilder(): InventoryUpdateBuilder {
273
+ return new InventoryUpdateBuilder();
274
+ }
275
+
276
+ async update(
277
+ builderOrCallback: InventoryUpdateBuilder | ((builder: InventoryUpdateBuilder) => void | Promise<void>),
278
+ options: InventoryBuilderOptions = {},
279
+ ): Promise<void> {
280
+ const builder = builderOrCallback instanceof InventoryUpdateBuilder ? builderOrCallback : this.createBuilder();
281
+ if (!(builderOrCallback instanceof InventoryUpdateBuilder)) {
282
+ await builderOrCallback(builder);
283
+ }
284
+
285
+ if (builder.isEmpty()) {
286
+ this.logger.debug('Inventory update skipped because builder produced no changes.');
287
+ return;
288
+ }
289
+
290
+ this.ensureScopes(['server']);
291
+ const payload = builder.toRequest(options);
292
+ await this.invokeInventoryPut(payload);
293
+ }
294
+
295
+ async setCurrencies(currencyValues: Record<string, bigint | number>, transaction?: string): Promise<void> {
296
+ this.ensureScopes(['server']);
297
+ const current = await this.getCurrencies(Object.keys(currencyValues));
298
+ const deltas: Record<string, bigint> = {};
299
+ for (const [currencyId, target] of Object.entries(currencyValues)) {
300
+ const currentVal = current[currencyId] ?? BigInt(0);
301
+ const targetValue = BigInt(target);
302
+ const delta = targetValue - currentVal;
303
+ if (delta !== BigInt(0)) {
304
+ deltas[currencyId] = delta;
305
+ }
306
+ }
307
+
308
+ if (Object.keys(deltas).length === 0) {
309
+ this.logger.debug('No currency adjustments required; request skipped.');
310
+ return;
311
+ }
312
+
313
+ await this.update((builder) => {
314
+ for (const [currencyId, delta] of Object.entries(deltas)) {
315
+ builder.currencyChange(currencyId, delta);
316
+ }
317
+ }, { transaction });
318
+ }
319
+
320
+ async addCurrencies(currencyDeltas: Record<string, bigint | number>, transaction?: string): Promise<void> {
321
+ await this.update((builder) => {
322
+ for (const [currencyId, delta] of Object.entries(currencyDeltas)) {
323
+ builder.currencyChange(currencyId, delta);
324
+ }
325
+ }, { transaction });
326
+ }
327
+
328
+ async addItem(contentId: string, properties: Record<string, string> = {}, transaction?: string): Promise<void> {
329
+ await this.update((builder) => {
330
+ builder.addItem(contentId, properties);
331
+ }, { transaction });
332
+ }
333
+
334
+ async deleteItem(contentId: string, itemId: bigint | number | string, transaction?: string): Promise<void> {
335
+ await this.update((builder) => {
336
+ builder.deleteItem(contentId, itemId);
337
+ }, { transaction });
338
+ }
339
+
340
+ async updateItem(
341
+ contentId: string,
342
+ itemId: bigint | number | string,
343
+ properties: Record<string, string>,
344
+ transaction?: string,
345
+ ): Promise<void> {
346
+ await this.update((builder) => {
347
+ builder.updateItem(contentId, itemId, properties);
348
+ }, { transaction });
349
+ }
350
+
351
+ async setCurrencyProperties(
352
+ currencyId: string,
353
+ properties: CurrencyProperty[],
354
+ transaction?: string,
355
+ ): Promise<void> {
356
+ await this.update((builder) => {
357
+ builder.setCurrencyProperties(currencyId, properties);
358
+ }, { transaction });
359
+ }
360
+
361
+ async previewCurrencyGain(currencyChanges: Record<string, bigint | number>): Promise<PreviewVipBonusResponse> {
362
+ this.ensureScopes(['server']);
363
+ const payload: InventoryUpdatePayload = {
364
+ currencies: Object.fromEntries(
365
+ Object.entries(currencyChanges).map(([currencyId, amount]) => [currencyId, BigInt(amount).toString()]),
366
+ ),
367
+ };
368
+ const response = await this.invokePreview(payload);
369
+ return response;
370
+ }
371
+
372
+ async getMultipliers(): Promise<MultipliersGetResponse> {
373
+ this.ensureScopes(['server']);
374
+ const response = await this.invokeGetMultipliers();
375
+ return response;
376
+ }
377
+
378
+ async getInventory(scope?: string): Promise<InventoryView> {
379
+ this.ensureScopes(['server']);
380
+ const response = await this.invokeGetInventory(scope);
381
+ return response;
382
+ }
383
+
384
+ async getCurrencies(currencyIds?: string[]): Promise<Record<string, bigint>> {
385
+ const scope = Array.isArray(currencyIds) && currencyIds.length > 0 ? currencyIds.join(',') : 'currency';
386
+ const view = await this.getInventory(scope);
387
+ const result: Record<string, bigint> = {};
388
+ for (const currency of view.currencies ?? []) {
389
+ result[currency.id] = BigInt(currency.amount);
390
+ }
391
+ return result;
392
+ }
393
+
394
+ async sendCurrency(
395
+ request: Omit<TransferRequestPayload, 'recipientPlayer'> & { recipientPlayer: string | number },
396
+ ): Promise<void> {
397
+ this.ensureScopes(['server']);
398
+ const payload: TransferRequestPayload = {
399
+ recipientPlayer: this.toIdentifier(request.recipientPlayer),
400
+ currencies: Object.fromEntries(
401
+ Object.entries(request.currencies ?? {}).map(([currencyId, amount]) => [currencyId, BigInt(amount).toString()]),
402
+ ),
403
+ transaction: request.transaction,
404
+ };
405
+ const response = this.api.inventoryPutTransferByObjectId?.(this.userId, payload);
406
+ if (!response || typeof (response as Promise<unknown>).then !== 'function') {
407
+ throw new Error('inventoryPutTransferByObjectId API is unavailable in the current SDK.');
408
+ }
409
+ await response;
410
+ }
411
+
412
+ private async invokeInventoryPut(payload: InventoryUpdatePayload): Promise<void> {
413
+ const response = this.api.inventoryPutByObjectId?.(this.userId, payload);
414
+ if (!response || typeof (response as Promise<unknown>).then !== 'function') {
415
+ throw new Error('inventoryPutByObjectId API is unavailable in the current SDK.');
416
+ }
417
+ await response;
418
+ }
419
+
420
+ private async invokePreview(payload: InventoryUpdatePayload): Promise<PreviewVipBonusResponse> {
421
+ const response = this.api.inventoryPutPreviewByObjectId?.(this.userId, payload);
422
+ if (!response || typeof (response as Promise<unknown>).then !== 'function') {
423
+ throw new Error('inventoryPutPreviewByObjectId API is unavailable in the current SDK.');
424
+ }
425
+ const result = await response;
426
+ if (!result || typeof result !== 'object' || !('body' in result)) {
427
+ return result as PreviewVipBonusResponse;
428
+ }
429
+ return (result as { body: PreviewVipBonusResponse }).body;
430
+ }
431
+
432
+ private async invokeGetMultipliers(): Promise<MultipliersGetResponse> {
433
+ const response = this.api.inventoryGetMultipliersByObjectId?.(this.userId);
434
+ if (!response || typeof (response as Promise<unknown>).then !== 'function') {
435
+ throw new Error('inventoryGetMultipliersByObjectId API is unavailable in the current SDK.');
436
+ }
437
+ const result = await response;
438
+ if (!result || typeof result !== 'object' || !('body' in result)) {
439
+ return result as MultipliersGetResponse;
440
+ }
441
+ return (result as { body: MultipliersGetResponse }).body;
442
+ }
443
+
444
+ private async invokeGetInventory(scope?: string): Promise<InventoryView> {
445
+ const response = this.api.inventoryGetByObjectId?.(this.userId, scope);
446
+ if (!response || typeof (response as Promise<unknown>).then !== 'function') {
447
+ throw new Error('inventoryGetByObjectId API is unavailable in the current SDK.');
448
+ }
449
+ const result = await response;
450
+ if (!result || typeof result !== 'object' || !('body' in result)) {
451
+ return result as InventoryView;
452
+ }
453
+ return (result as { body: InventoryView }).body;
454
+ }
455
+
456
+ private toIdentifier(value: string | number | bigint): string {
457
+ if (typeof value === 'string') {
458
+ const trimmed = value.trim();
459
+ if (!trimmed) {
460
+ throw new Error('Identifier cannot be empty.');
461
+ }
462
+ return trimmed;
463
+ }
464
+ if (typeof value === 'number') {
465
+ if (!Number.isFinite(value)) {
466
+ throw new Error(`Invalid numeric identifier: ${value}`);
467
+ }
468
+ return Math.trunc(value).toString();
469
+ }
470
+ return value.toString();
471
+ }
472
+ }
473
+
474
+ export function createInventoryService(
475
+ api: BoundBeamApi,
476
+ logger: Logger,
477
+ userId: string,
478
+ hasScopes: (scopes: string[]) => boolean,
479
+ ): InventoryService {
480
+ return new InventoryService({
481
+ api,
482
+ logger,
483
+ userId,
484
+ scopeChecker: (requiredScopes: string[]) => {
485
+ if (!hasScopes(requiredScopes)) {
486
+ throw new MissingScopesError(requiredScopes);
487
+ }
488
+ },
489
+ });
490
+ }
491
+
package/src/logger.ts ADDED
@@ -0,0 +1,38 @@
1
+ import pino, { destination, type Logger, type LoggerOptions } from 'pino';
2
+ import { ensureWritableTempDirectory } from './env.js';
3
+ import type { EnvironmentConfig } from './types.js';
4
+
5
+ interface LoggerFactoryOptions {
6
+ name?: string;
7
+ destinationPath?: string;
8
+ }
9
+
10
+ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptions = {}): Logger {
11
+ const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;
12
+
13
+ const pinoOptions: LoggerOptions = {
14
+ name: options.name ?? 'beamable-node-runtime',
15
+ level: env.logLevel,
16
+ base: {
17
+ cid: env.cid,
18
+ pid: env.pid,
19
+ routingKey: env.routingKey ?? null,
20
+ sdkVersionExecution: env.sdkVersionExecution,
21
+ },
22
+ redact: {
23
+ paths: ['secret', 'refreshToken'],
24
+ censor: '***',
25
+ },
26
+ };
27
+
28
+ // For deployed services, always log to stdout so container orchestrator can collect logs
29
+ // For local development, log to stdout unless a specific file path is provided
30
+ if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {
31
+ // Log to stdout (default pino behavior) - this is critical for container log collection
32
+ return pino(pinoOptions, process.stdout);
33
+ }
34
+
35
+ const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;
36
+ const stream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
37
+ return pino(pinoOptions, stream);
38
+ }
package/src/message.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { GatewayRequest, GatewayResponse, WebsocketEventEnvelope } from './types.js';
2
+
3
+ let nextRequestId = -1;
4
+
5
+ export function allocateRequestId(): number {
6
+ nextRequestId -= 1;
7
+ if (nextRequestId < -9_000_000_000) {
8
+ nextRequestId = -1;
9
+ }
10
+ return nextRequestId;
11
+ }
12
+
13
+ export function serializeGatewayRequest(request: GatewayRequest): string {
14
+ return JSON.stringify(request);
15
+ }
16
+
17
+ export function deserializeGatewayResponse(raw: string): GatewayResponse | WebsocketEventEnvelope {
18
+ return JSON.parse(raw) as GatewayResponse | WebsocketEventEnvelope;
19
+ }
@@ -0,0 +1,126 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Logger } from 'pino';
3
+ import { AuthenticationError, TimeoutError } from './errors.js';
4
+ import { allocateRequestId, deserializeGatewayResponse, serializeGatewayRequest } from './message.js';
5
+ import { BeamableWebSocket } from './websocket.js';
6
+ import type { GatewayRequest, GatewayResponse, WebsocketEventEnvelope } from './types.js';
7
+
8
+ interface PendingRequest<T = unknown> {
9
+ resolve: (value: T) => void;
10
+ reject: (error: Error) => void;
11
+ timeoutHandle: NodeJS.Timeout;
12
+ path: string;
13
+ method: string;
14
+ }
15
+
16
+ export class GatewayRequester {
17
+ private readonly socket: BeamableWebSocket;
18
+ private readonly logger: Logger;
19
+ private readonly pending = new Map<number, PendingRequest>();
20
+ private readonly emitter = new EventEmitter();
21
+ private requestTimeoutMs = 15_000;
22
+
23
+ constructor(socket: BeamableWebSocket, logger: Logger) {
24
+ this.socket = socket;
25
+ this.logger = logger.child({ component: 'GatewayRequester' });
26
+ this.socket.on('message', (...args) => this.onRawMessage(String(args[0] ?? '')));
27
+ }
28
+
29
+ on(event: 'event', listener: (envelope: WebsocketEventEnvelope) => void): void {
30
+ this.emitter.on(event, listener);
31
+ }
32
+
33
+ off(event: 'event', listener: (envelope: WebsocketEventEnvelope) => void): void {
34
+ this.emitter.off(event, listener);
35
+ }
36
+
37
+ setRequestTimeout(timeoutMs: number): void {
38
+ this.requestTimeoutMs = timeoutMs;
39
+ }
40
+
41
+ async request<T>(method: string, path: string, body?: unknown): Promise<T> {
42
+ const id = allocateRequestId();
43
+ const effectiveBody = body ?? {};
44
+ const request: GatewayRequest = {
45
+ id,
46
+ method: method.toLowerCase(),
47
+ path,
48
+ body: effectiveBody,
49
+ };
50
+
51
+ this.logger.debug({ id, method, path, body: effectiveBody }, 'Sending gateway request.');
52
+
53
+ const payload = serializeGatewayRequest(request);
54
+
55
+ return new Promise<T>((resolve, reject) => {
56
+ const timeoutHandle = setTimeout(() => {
57
+ this.pending.delete(id);
58
+ reject(new TimeoutError(`Request ${method} ${path} timed out after ${this.requestTimeoutMs}ms`));
59
+ }, this.requestTimeoutMs).unref();
60
+
61
+ this.pending.set(id, {
62
+ resolve: resolve as (value: unknown) => void,
63
+ reject,
64
+ timeoutHandle,
65
+ path,
66
+ method,
67
+ });
68
+
69
+ this.socket
70
+ .send(payload)
71
+ .catch((error) => {
72
+ clearTimeout(timeoutHandle);
73
+ this.pending.delete(id);
74
+ reject(error instanceof Error ? error : new Error(String(error)));
75
+ });
76
+ });
77
+ }
78
+
79
+ async sendResponse(response: GatewayResponse): Promise<void> {
80
+ const payload = JSON.stringify(response);
81
+ await this.socket.send(payload);
82
+ }
83
+
84
+ async acknowledge(id: number, status = 200): Promise<void> {
85
+ await this.sendResponse({ id, status, body: null });
86
+ }
87
+
88
+ private onRawMessage(raw: string): void {
89
+ this.logger.debug({ raw }, 'Received websocket frame.');
90
+ let envelope: GatewayResponse | WebsocketEventEnvelope;
91
+ try {
92
+ envelope = deserializeGatewayResponse(raw);
93
+ } catch (error) {
94
+ this.logger.error({ err: error, raw }, 'Failed to parse gateway message.');
95
+ return;
96
+ }
97
+
98
+ if (typeof envelope.id !== 'number') {
99
+ this.logger.warn({ envelope }, 'Invalid gateway message without id.');
100
+ return;
101
+ }
102
+
103
+ const pending = this.pending.get(envelope.id);
104
+ if (pending) {
105
+ clearTimeout(pending.timeoutHandle);
106
+ this.pending.delete(envelope.id);
107
+ if ('status' in envelope && envelope.status === 403) {
108
+ pending.reject(new AuthenticationError('Gateway rejected the request with 403.'));
109
+ return;
110
+ }
111
+ pending.resolve(envelope.body as unknown);
112
+ return;
113
+ }
114
+
115
+ this.emitter.emit('event', envelope as WebsocketEventEnvelope);
116
+ }
117
+
118
+ dispose(): void {
119
+ for (const [id, pending] of this.pending.entries()) {
120
+ clearTimeout(pending.timeoutHandle);
121
+ pending.reject(new Error(`Request ${pending.method} ${pending.path} cancelled during shutdown.`));
122
+ this.pending.delete(id);
123
+ }
124
+ this.emitter.removeAllListeners();
125
+ }
126
+ }