@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/services.ts DELETED
@@ -1,459 +0,0 @@
1
- import type { Logger } from 'pino';
2
- import {
3
- BeamServer,
4
- BeamEnvironment,
5
- defaultTokenStorage,
6
- type HttpRequester,
7
- AccountService,
8
- AnnouncementsService,
9
- AuthService,
10
- ContentService,
11
- LeaderboardsService,
12
- StatsService,
13
- } from 'beamable-sdk';
14
- import type { EnvironmentConfig } from './types.js';
15
- import { hostToHttpUrl, hostToPortalUrl, hostToStorageUrl, hostToMicroserviceRegistryUrl } from './utils/urls.js';
16
- import { createInventoryService, type InventoryService } from './inventory.js';
17
- import { StorageService } from './storage.js';
18
- import type { FederationRegistry } from './federation.js';
19
- import { MissingScopesError } from './errors.js';
20
-
21
- const RUNTIME_ENVIRONMENT_NAME = 'node-runtime';
22
-
23
- export type BoundBeamApi = Record<string, (...args: unknown[]) => unknown>;
24
-
25
- let cachedBeamApiModule: Record<string, unknown> | null = null;
26
-
27
- async function resolveBeamApiModule(): Promise<Record<string, unknown>> {
28
- if (!cachedBeamApiModule) {
29
- const imported = await import('beamable-sdk/api');
30
- cachedBeamApiModule =
31
- imported && typeof imported === 'object' && 'default' in imported
32
- ? (imported.default as Record<string, unknown>)
33
- : (imported as Record<string, unknown>);
34
- }
35
- return cachedBeamApiModule;
36
- }
37
-
38
- async function bindBeamApi(requester: HttpRequester): Promise<BoundBeamApi> {
39
- const module = await resolveBeamApiModule();
40
- const bound: Record<string, (...args: unknown[]) => unknown> = {};
41
- for (const key of Object.keys(module)) {
42
- const value = module[key];
43
- if (typeof value === 'function') {
44
- bound[key] = (...args: unknown[]) => (value as (r: HttpRequester, ...params: unknown[]) => unknown)(requester, ...args);
45
- }
46
- }
47
- return bound;
48
- }
49
-
50
- function createFailingApi(error: Error): BoundBeamApi {
51
- return new Proxy(
52
- {},
53
- {
54
- get() {
55
- throw error;
56
- },
57
- },
58
- ) as BoundBeamApi;
59
- }
60
-
61
- type ServiceMap = {
62
- account: AccountService;
63
- announcements: AnnouncementsService;
64
- auth: AuthService;
65
- content: ContentService;
66
- leaderboards: LeaderboardsService;
67
- stats: StatsService;
68
- };
69
-
70
- type ServiceKey = keyof ServiceMap;
71
-
72
- class UnavailableRequester implements HttpRequester {
73
- constructor(private readonly error: Error) {}
74
-
75
- async request(): Promise<never> {
76
- throw this.error;
77
- }
78
-
79
- set baseUrl(_: string) {
80
- // no-op
81
- }
82
-
83
- set defaultHeaders(_: Record<string, string>) {
84
- // no-op
85
- }
86
- }
87
-
88
- class UnavailableBeamableServices implements BeamableMicroserviceServices {
89
- private readonly requesterImpl: HttpRequester;
90
- private readonly apiBindings: BoundBeamApi;
91
-
92
- constructor(private readonly error: Error) {
93
- this.requesterImpl = new UnavailableRequester(this.error);
94
- this.apiBindings = createFailingApi(this.error);
95
- }
96
-
97
- private fail(): never {
98
- throw this.error;
99
- }
100
-
101
- get account(): AccountService {
102
- return this.fail();
103
- }
104
-
105
- get announcements(): AnnouncementsService {
106
- return this.fail();
107
- }
108
-
109
- get auth(): AuthService {
110
- return this.fail();
111
- }
112
-
113
- get content(): ContentService {
114
- return this.fail();
115
- }
116
-
117
- get leaderboards(): LeaderboardsService {
118
- return this.fail();
119
- }
120
-
121
- get stats(): StatsService {
122
- return this.fail();
123
- }
124
-
125
- get inventory(): InventoryService {
126
- return this.fail();
127
- }
128
-
129
- get storage(): StorageService {
130
- return this.fail();
131
- }
132
-
133
- get federation(): FederationRegistry {
134
- return this.fail();
135
- }
136
-
137
- get api(): BoundBeamApi {
138
- return this.apiBindings;
139
- }
140
-
141
- get requester(): HttpRequester {
142
- return this.requesterImpl;
143
- }
144
-
145
- hasScopes(..._scopes: string[]): boolean {
146
- return false;
147
- }
148
-
149
- requireScopes(...scopes: string[]): void {
150
- throw new MissingScopesError(scopes);
151
- }
152
-
153
- assumeUser(): BeamableMicroserviceServices {
154
- return this;
155
- }
156
- }
157
-
158
- class BeamableServicesFacade implements BeamableMicroserviceServices {
159
- private readonly cache: Partial<Record<ServiceKey, ServiceMap[ServiceKey]>> = {};
160
- private readonly apiBindings: BoundBeamApi;
161
- private readonly storageService: StorageService;
162
- private readonly scopes: Set<string>;
163
- private readonly logger: Logger;
164
- private readonly serviceName?: string;
165
- private readonly federationRegistry?: FederationRegistry;
166
- private inventoryClient?: InventoryService;
167
-
168
- constructor(
169
- private readonly manager: BeamableServiceManager,
170
- private readonly beamServer: BeamServer,
171
- private readonly userId: string,
172
- scopes: Set<string>,
173
- serviceName?: string,
174
- ) {
175
- this.apiBindings = this.manager.getBoundApi();
176
- this.storageService = this.manager.getStorageService();
177
- this.scopes = scopes;
178
- this.serviceName = serviceName;
179
- this.federationRegistry = serviceName ? this.manager.getFederationRegistry(serviceName) : undefined;
180
- this.logger = this.manager.getLogger().child({ component: 'BeamableServicesFacade', service: serviceName });
181
- }
182
-
183
- private getOrCreate<K extends ServiceKey>(key: K, factory: () => ServiceMap[K]): ServiceMap[K] {
184
- if (!this.cache[key]) {
185
- this.cache[key] = factory();
186
- }
187
- return this.cache[key] as ServiceMap[K];
188
- }
189
-
190
- get account(): AccountService {
191
- return this.getOrCreate('account', () => this.beamServer.account(this.userId));
192
- }
193
-
194
- get announcements(): AnnouncementsService {
195
- return this.getOrCreate('announcements', () => this.beamServer.announcements(this.userId));
196
- }
197
-
198
- get auth(): AuthService {
199
- return this.getOrCreate('auth', () => this.beamServer.auth(this.userId));
200
- }
201
-
202
- get content(): ContentService {
203
- return this.getOrCreate('content', () => this.beamServer.content(this.userId));
204
- }
205
-
206
- get leaderboards(): LeaderboardsService {
207
- return this.getOrCreate('leaderboards', () => this.beamServer.leaderboards(this.userId));
208
- }
209
-
210
- get stats(): StatsService {
211
- return this.getOrCreate('stats', () => this.beamServer.stats(this.userId));
212
- }
213
-
214
- get inventory(): InventoryService {
215
- if (!this.inventoryClient) {
216
- this.inventoryClient = createInventoryService(
217
- this.apiBindings,
218
- this.logger.child({ component: 'InventoryClient' }),
219
- this.userId,
220
- (requiredScopes) => this.hasScopes(...requiredScopes),
221
- );
222
- }
223
- return this.inventoryClient;
224
- }
225
-
226
- get storage(): StorageService {
227
- return this.storageService;
228
- }
229
-
230
- get federation(): FederationRegistry {
231
- if (!this.federationRegistry) {
232
- throw new Error(
233
- this.serviceName
234
- ? `Federation registry is not configured for service "${this.serviceName}".`
235
- : 'Federation registry is not configured.',
236
- );
237
- }
238
- return this.federationRegistry;
239
- }
240
-
241
- get api(): BoundBeamApi {
242
- return this.apiBindings;
243
- }
244
-
245
- get requester(): HttpRequester {
246
- return this.manager.getRequester();
247
- }
248
-
249
- hasScopes(...requiredScopes: string[]): boolean {
250
- if (requiredScopes.length === 0) {
251
- return true;
252
- }
253
- return requiredScopes.every((scope) => this.scopeSetHas(scope));
254
- }
255
-
256
- requireScopes(...requiredScopes: string[]): void {
257
- if (!this.hasScopes(...requiredScopes)) {
258
- throw new MissingScopesError(requiredScopes);
259
- }
260
- }
261
-
262
- assumeUser(userId: string | number): BeamableMicroserviceServices {
263
- return this.manager.createFacade(String(userId), new Set(this.scopes), this.serviceName);
264
- }
265
-
266
- private scopeSetHas(scope: string): boolean {
267
- const normalized = scope.trim().toLowerCase();
268
- if (!normalized) {
269
- return false;
270
- }
271
- return this.scopes.has('*') || this.scopes.has(normalized);
272
- }
273
- }
274
-
275
- export interface BeamableMicroserviceServices {
276
- readonly account: AccountService;
277
- readonly announcements: AnnouncementsService;
278
- readonly auth: AuthService;
279
- readonly content: ContentService;
280
- readonly leaderboards: LeaderboardsService;
281
- readonly stats: StatsService;
282
- readonly inventory: InventoryService;
283
- readonly storage: StorageService;
284
- readonly federation: FederationRegistry;
285
- readonly api: BoundBeamApi;
286
- readonly requester: HttpRequester;
287
- hasScopes(...scopes: string[]): boolean;
288
- requireScopes(...scopes: string[]): void;
289
- assumeUser(userId: string | number): BeamableMicroserviceServices;
290
- }
291
-
292
- export class BeamableServiceManager {
293
- private readonly logger: Logger;
294
- private readonly tokenStorage: ReturnType<typeof defaultTokenStorage>;
295
- private readonly env: EnvironmentConfig;
296
- private beamServer?: BeamServer;
297
- private boundApi?: BoundBeamApi;
298
- private storageService?: StorageService;
299
- private initialized = false;
300
- private readonly federationRegistries = new Map<string, FederationRegistry>();
301
-
302
- constructor(env: EnvironmentConfig, logger: Logger) {
303
- this.env = env;
304
- this.logger = logger.child({ component: 'BeamableServiceManager' });
305
- this.tokenStorage = defaultTokenStorage({ pid: env.pid, tag: env.routingKey ?? RUNTIME_ENVIRONMENT_NAME });
306
- }
307
-
308
- async initialize(): Promise<void> {
309
- if (this.initialized) {
310
- return;
311
- }
312
-
313
- this.registerEnvironment();
314
- this.configureSharedEnvironment();
315
-
316
- if (this.env.refreshToken) {
317
- await this.tokenStorage.setTokenData({ refreshToken: this.env.refreshToken });
318
- }
319
-
320
- const useSignedRequest = Boolean(this.env.secret);
321
- if (!useSignedRequest && !this.env.refreshToken) {
322
- this.logger.warn(
323
- 'Beamable realm secret and refresh token are both missing; Beamable SDK requests may fail due to missing credentials.',
324
- );
325
- }
326
-
327
- try {
328
- this.beamServer = await BeamServer.init({
329
- cid: this.env.cid,
330
- pid: this.env.pid,
331
- environment: RUNTIME_ENVIRONMENT_NAME,
332
- useSignedRequest,
333
- tokenStorage: this.tokenStorage,
334
- serverEvents: { enabled: false },
335
- });
336
- } catch (error) {
337
- this.logger.error({ err: error }, 'Failed to initialize Beamable SDK. Services layer will be unavailable.');
338
- this.beamServer = undefined;
339
- this.initialized = false;
340
- return;
341
- }
342
-
343
- this.boundApi = await bindBeamApi(this.beamServer.requester);
344
-
345
- // Configure the requester's default headers to include the routing key in the correct format
346
- // The routing key format is: serviceName:routingKey
347
- // We'll set this per-request in createFacade since we need the service name
348
- this.storageService = new StorageService({
349
- requester: this.beamServer.requester,
350
- api: this.boundApi,
351
- env: this.env,
352
- logger: this.logger.child({ component: 'StorageService' }),
353
- });
354
-
355
- this.registerDefaultServices();
356
- this.initialized = true;
357
- this.logger.debug('Beamable SDK services initialized.');
358
- }
359
-
360
- createFacade(userId: string | number, scopes: Set<string>, serviceName?: string): BeamableMicroserviceServices {
361
- if (!this.initialized || !this.beamServer || !this.boundApi || !this.storageService) {
362
- return new UnavailableBeamableServices(new Error('Beamable services are not initialized.'));
363
- }
364
-
365
- // Set the routing key in the format serviceName:routingKey for the SDK
366
- // The SDK uses BEAM_ROUTING_KEY environment variable to construct the X-BEAM-SERVICE-ROUTING-KEY header
367
- // We need to set it to the full format: serviceName:routingKey
368
- if (serviceName && this.beamServer.requester) {
369
- const fullRoutingKey = `micro_${serviceName}:${this.env.routingKey}`;
370
- BeamServer.env.BEAM_ROUTING_KEY = fullRoutingKey;
371
-
372
- // Set default headers on the requester for all SDK requests
373
- // These headers are required for signature validation and routing
374
- const scopeHeader = `${this.env.cid}.${this.env.pid}`;
375
- this.beamServer.requester.defaultHeaders = {
376
- ...this.beamServer.requester.defaultHeaders,
377
- 'X-BEAM-SCOPE': scopeHeader,
378
- 'X-BEAM-SERVICE-ROUTING-KEY': fullRoutingKey,
379
- };
380
- }
381
-
382
- return new BeamableServicesFacade(this, this.beamServer, String(userId), new Set(scopes), serviceName);
383
- }
384
-
385
- getRequester(): HttpRequester {
386
- if (!this.initialized || !this.beamServer) {
387
- return new UnavailableRequester(new Error('Beamable services are not initialized.'));
388
- }
389
- return this.beamServer.requester;
390
- }
391
-
392
- getBoundApi(): BoundBeamApi {
393
- if (!this.boundApi) {
394
- throw new Error('Beamable bound API is not available.');
395
- }
396
- return this.boundApi;
397
- }
398
-
399
- getStorageService(): StorageService {
400
- if (!this.storageService) {
401
- throw new Error('Storage service is not initialized.');
402
- }
403
- return this.storageService;
404
- }
405
-
406
- getLogger(): Logger {
407
- return this.logger;
408
- }
409
-
410
- registerFederationRegistry(serviceName: string, registry: FederationRegistry): void {
411
- this.federationRegistries.set(serviceName, registry);
412
- }
413
-
414
- getFederationRegistry(serviceName: string): FederationRegistry | undefined {
415
- return this.federationRegistries.get(serviceName);
416
- }
417
-
418
- private registerEnvironment(): void {
419
- const apiUrl = hostToHttpUrl(this.env.host);
420
- const portalUrl = hostToPortalUrl(apiUrl);
421
- const storageUrl = hostToStorageUrl(apiUrl);
422
- const registryUrl = hostToMicroserviceRegistryUrl(apiUrl);
423
-
424
- try {
425
- BeamEnvironment.register(RUNTIME_ENVIRONMENT_NAME, {
426
- apiUrl,
427
- portalUrl,
428
- beamMongoExpressUrl: storageUrl,
429
- dockerRegistryUrl: `${registryUrl}/v2/`,
430
- });
431
- } catch (error) {
432
- this.logger.debug({ err: error }, 'Beamable environment was already registered. Reusing existing configuration.');
433
- }
434
- }
435
-
436
- private configureSharedEnvironment(): void {
437
- if (this.env.secret) {
438
- BeamServer.env.BEAM_REALM_SECRET = this.env.secret;
439
- }
440
- // BEAM_ROUTING_KEY is used by the SDK to construct the X-BEAM-SERVICE-ROUTING-KEY header
441
- // We set it to just the routing key value here, and will set the full format in createFacade
442
- // BEAM_ROUTING_KEY is used by the SDK to construct the X-BEAM-SERVICE-ROUTING-KEY header
443
- // For deployed services, routingKey is undefined, so we set it to empty string
444
- // The SDK will handle this appropriately
445
- BeamServer.env.BEAM_ROUTING_KEY = this.env.routingKey ?? '';
446
- }
447
-
448
- private registerDefaultServices(): void {
449
- if (!this.beamServer) {
450
- return;
451
- }
452
- this.beamServer.use(AccountService);
453
- this.beamServer.use(AnnouncementsService);
454
- this.beamServer.use(AuthService);
455
- this.beamServer.use(ContentService);
456
- this.beamServer.use(LeaderboardsService);
457
- this.beamServer.use(StatsService);
458
- }
459
- }
package/src/storage.ts DELETED
@@ -1,206 +0,0 @@
1
- import type { Logger } from 'pino';
2
- import { MongoClient, type Db, type Collection, type Document } from 'mongodb';
3
- import type { BoundBeamApi } from './services.js';
4
- import type { EnvironmentConfig } from './types.js';
5
- import type { HttpRequester } from 'beamable-sdk';
6
-
7
- export interface StorageConnectionOptions {
8
- useCache?: boolean;
9
- }
10
-
11
- export interface StorageCollectionOptions extends StorageConnectionOptions {
12
- collectionName?: string;
13
- }
14
-
15
- export interface StorageMetadata {
16
- storageName: string;
17
- }
18
-
19
- const STORAGE_OBJECT_METADATA = new Map<Function, StorageMetadata>();
20
-
21
- export function StorageObject(storageName: string): ClassDecorator {
22
- if (!storageName || !storageName.trim()) {
23
- throw new Error('@StorageObject requires a non-empty storage name.');
24
- }
25
- return (target) => {
26
- STORAGE_OBJECT_METADATA.set(target, { storageName: storageName.trim() });
27
- };
28
- }
29
-
30
- export function getStorageMetadata(target: Function): StorageMetadata | undefined {
31
- return STORAGE_OBJECT_METADATA.get(target);
32
- }
33
-
34
- export function listRegisteredStorageObjects(): StorageMetadata[] {
35
- return Array.from(STORAGE_OBJECT_METADATA.values());
36
- }
37
-
38
- interface StorageServiceDependencies {
39
- requester: HttpRequester;
40
- api: BoundBeamApi;
41
- env: EnvironmentConfig;
42
- logger: Logger;
43
- }
44
-
45
- interface ConnectionStringResponse {
46
- connectionString: string;
47
- }
48
-
49
- const CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';
50
-
51
- export class StorageService {
52
- private readonly requester: HttpRequester;
53
- private readonly api: BoundBeamApi;
54
- private readonly env: EnvironmentConfig;
55
- private readonly logger: Logger;
56
- private readonly databaseCache = new Map<string, Db>();
57
- private readonly clientCache = new Map<string, MongoClient>();
58
- private cachedConnectionString?: string;
59
-
60
- constructor(dependencies: StorageServiceDependencies) {
61
- this.requester = dependencies.requester;
62
- this.api = dependencies.api;
63
- this.env = dependencies.env;
64
- this.logger = dependencies.logger.child({ component: 'StorageService' });
65
- }
66
-
67
- async getDatabase(storageName: string, options: StorageConnectionOptions = {}): Promise<Db> {
68
- const normalized = this.normalizeStorageName(storageName);
69
- if (!options.useCache) {
70
- this.databaseCache.delete(normalized);
71
- }
72
- const cached = this.databaseCache.get(normalized);
73
- if (cached) {
74
- return cached;
75
- }
76
-
77
- const connectionString = await this.getConnectionString(normalized);
78
- const client = await this.getMongoClient(connectionString);
79
- const databaseName = this.buildDatabaseName(normalized);
80
- const database = client.db(databaseName);
81
- this.databaseCache.set(normalized, database);
82
- return database;
83
- }
84
-
85
- async getDatabaseFor<T>(storageCtor: new () => T, options: StorageConnectionOptions = {}): Promise<Db> {
86
- const metadata = getStorageMetadata(storageCtor);
87
- if (!metadata) {
88
- throw new Error(
89
- `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,
90
- );
91
- }
92
- return this.getDatabase(metadata.storageName, options);
93
- }
94
-
95
- async getCollection<TDocument extends Document>(
96
- storageName: string,
97
- options: StorageCollectionOptions = {},
98
- ): Promise<Collection<TDocument>> {
99
- const database = await this.getDatabase(storageName, options);
100
- const collectionName = options.collectionName?.trim();
101
- if (!collectionName) {
102
- throw new Error('Collection name must be provided when using getCollection with raw storage name.');
103
- }
104
- return database.collection<TDocument>(collectionName);
105
- }
106
-
107
- async getCollectionFor<TStorage, TDocument extends Document>(
108
- storageCtor: new () => TStorage,
109
- collectionCtor: new () => TDocument,
110
- options: StorageCollectionOptions = {},
111
- ): Promise<Collection<TDocument>> {
112
- const metadata = getStorageMetadata(storageCtor);
113
- if (!metadata) {
114
- throw new Error(
115
- `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,
116
- );
117
- }
118
- const collectionName = options.collectionName?.trim() ?? collectionCtor.name;
119
- const database = await this.getDatabase(metadata.storageName, options);
120
- return database.collection<TDocument>(collectionName);
121
- }
122
-
123
- private async getMongoClient(connectionString: string): Promise<MongoClient> {
124
- if (this.clientCache.has(connectionString)) {
125
- return this.clientCache.get(connectionString) as MongoClient;
126
- }
127
- const client = new MongoClient(connectionString, {
128
- maxPoolSize: 20,
129
- });
130
- await client.connect();
131
- this.clientCache.set(connectionString, client);
132
- return client;
133
- }
134
-
135
- private async getConnectionString(storageName: string): Promise<string> {
136
- const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;
137
- const envValue = process.env[variableName];
138
- if (envValue && envValue.trim()) {
139
- return envValue.trim();
140
- }
141
-
142
- if (this.cachedConnectionString) {
143
- return this.cachedConnectionString;
144
- }
145
-
146
- const response = await this.fetchConnectionString();
147
- if (!response.connectionString || !response.connectionString.trim()) {
148
- throw new Error(`Connection string for storage "${storageName}" is empty.`);
149
- }
150
- this.cachedConnectionString = response.connectionString.trim();
151
- return this.cachedConnectionString;
152
- }
153
-
154
- private async fetchConnectionString(): Promise<ConnectionStringResponse> {
155
- if (typeof this.api.beamoGetStorageConnectionBasic === 'function') {
156
- const result = await this.api.beamoGetStorageConnectionBasic();
157
- if (result && typeof result === 'object' && 'body' in result) {
158
- return (result as { body: ConnectionStringResponse }).body;
159
- }
160
- return result as ConnectionStringResponse;
161
- }
162
-
163
- this.logger.warn(
164
- 'beamable-sdk does not expose beamoGetStorageConnectionBasic; falling back to manual requester call.',
165
- );
166
- const response = await this.requester.request({
167
- method: 'GET',
168
- url: '/basic/beamo/storage/connection',
169
- withAuth: true,
170
- });
171
- const body = response.body as ConnectionStringResponse | undefined;
172
- if (!body || typeof body.connectionString !== 'string') {
173
- throw new Error('Failed to retrieve Beamable storage connection string.');
174
- }
175
- return body;
176
- }
177
-
178
- private buildDatabaseName(storageName: string): string {
179
- const cid = this.sanitize(this.env.cid);
180
- const pid = this.sanitize(this.env.pid);
181
- const storage = this.sanitize(storageName);
182
- return `${cid}${pid}_${storage}`;
183
- }
184
-
185
- private sanitize(value: string): string {
186
- return value.replace(/[^A-Za-z0-9_]/g, '_');
187
- }
188
-
189
- private normalizeStorageName(storageName: string): string {
190
- const normalized = storageName.trim();
191
- if (!normalized) {
192
- throw new Error('Storage name cannot be empty.');
193
- }
194
- return normalized;
195
- }
196
-
197
- async dispose(): Promise<void> {
198
- for (const client of this.clientCache.values()) {
199
- await client.close();
200
- }
201
- this.clientCache.clear();
202
- this.databaseCache.clear();
203
- this.cachedConnectionString = undefined;
204
- }
205
- }
206
-
@@ -1,5 +0,0 @@
1
- declare module 'beamable-sdk/api' {
2
- const api: Record<string, unknown>;
3
- export default api;
4
- }
5
-