@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,588 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import readline from 'node:readline';
7
+ import { stdin, stdout, exit } from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import dotenv from 'dotenv';
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const SERVICE_NAME = 'beamo-node';
12
+ const CONFIG_DIR = path.join(os.homedir(), '.beamo-node');
13
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
14
+ const TOKEN_FALLBACK_DIR = path.join(CONFIG_DIR, 'tokens');
15
+ const DEFAULT_SOCKET_HOST = 'wss://api.beamable.com/socket';
16
+ const MIN_TOKEN_BUFFER_MS = 60_000;
17
+ class CredentialStore {
18
+ keytar;
19
+ constructor(keytar) {
20
+ this.keytar = keytar;
21
+ }
22
+ static async create() {
23
+ try {
24
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
25
+ const keytarModule = (await import('keytar')).default;
26
+ return new CredentialStore(keytarModule);
27
+ }
28
+ catch (error) {
29
+ console.warn('[beamo-node] keytar not available, falling back to file-based credential storage.');
30
+ return new CredentialStore();
31
+ }
32
+ }
33
+ async get(account) {
34
+ if (this.keytar) {
35
+ const payload = await this.keytar.getPassword(SERVICE_NAME, account);
36
+ if (!payload) {
37
+ return undefined;
38
+ }
39
+ return this.parseTokenPayload(payload);
40
+ }
41
+ const filePath = this.fallbackPath(account);
42
+ try {
43
+ const payload = await fs.readFile(filePath, 'utf8');
44
+ return this.parseTokenPayload(payload);
45
+ }
46
+ catch (error) {
47
+ if (error.code === 'ENOENT') {
48
+ return undefined;
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ async set(account, record) {
54
+ const payload = JSON.stringify(record);
55
+ if (this.keytar) {
56
+ await this.keytar.setPassword(SERVICE_NAME, account, payload);
57
+ return;
58
+ }
59
+ await fs.mkdir(TOKEN_FALLBACK_DIR, { recursive: true });
60
+ const filePath = this.fallbackPath(account);
61
+ await fs.writeFile(filePath, payload, { mode: 0o600, encoding: 'utf8' });
62
+ }
63
+ async clear(account) {
64
+ if (this.keytar) {
65
+ await this.keytar.deletePassword(SERVICE_NAME, account);
66
+ return;
67
+ }
68
+ try {
69
+ await fs.unlink(this.fallbackPath(account));
70
+ }
71
+ catch (error) {
72
+ if (error.code !== 'ENOENT') {
73
+ throw error;
74
+ }
75
+ }
76
+ }
77
+ parseTokenPayload(payload) {
78
+ const parsed = JSON.parse(payload);
79
+ if (!parsed || typeof parsed !== 'object') {
80
+ throw new Error('Stored credentials are malformed.');
81
+ }
82
+ if (typeof parsed.accessToken !== 'string' || typeof parsed.refreshToken !== 'string') {
83
+ throw new Error('Stored credentials missing access or refresh token.');
84
+ }
85
+ return {
86
+ accessToken: parsed.accessToken,
87
+ refreshToken: parsed.refreshToken,
88
+ expiresAt: typeof parsed.expiresAt === 'number' ? parsed.expiresAt : undefined,
89
+ };
90
+ }
91
+ fallbackPath(account) {
92
+ const safe = Buffer.from(account).toString('base64url');
93
+ return path.join(TOKEN_FALLBACK_DIR, `${safe}.json`);
94
+ }
95
+ }
96
+ class ConfigStore {
97
+ async load() {
98
+ try {
99
+ const payload = await fs.readFile(CONFIG_FILE, 'utf8');
100
+ const data = JSON.parse(payload);
101
+ return {
102
+ profiles: data?.profiles ?? {},
103
+ lastUsed: data?.lastUsed,
104
+ };
105
+ }
106
+ catch (error) {
107
+ if (error.code === 'ENOENT') {
108
+ return { profiles: {} };
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ async save(config) {
114
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
115
+ const payload = JSON.stringify(config, null, 2);
116
+ await fs.writeFile(CONFIG_FILE, `${payload}\n`, { encoding: 'utf8', mode: 0o600 });
117
+ }
118
+ }
119
+ class TokenManager {
120
+ store;
121
+ constructor(store) {
122
+ this.store = store;
123
+ }
124
+ async login(options) {
125
+ const apiHost = normalizeApiHost(options.host);
126
+ // Scope should be cid.pid if both are provided (matches official CLI behavior)
127
+ const scope = options.pid ? `${options.cid}.${options.pid}` : options.cid;
128
+ // Admin accounts require customerScoped: true (matches C# CLI behavior)
129
+ // The registry will accept customer-scoped tokens when proper headers are used
130
+ const customerScoped = true;
131
+ const response = await fetch(new URL('/basic/auth/token', apiHost), {
132
+ method: 'POST',
133
+ headers: {
134
+ Accept: 'application/json',
135
+ 'Content-Type': 'application/json',
136
+ ...(scope ? { 'X-BEAM-SCOPE': scope } : {}),
137
+ },
138
+ body: JSON.stringify({
139
+ grant_type: 'password',
140
+ username: options.email,
141
+ password: options.password,
142
+ customerScoped,
143
+ }),
144
+ });
145
+ if (!response.ok) {
146
+ const message = await safeReadError(response);
147
+ throw new Error(`Login failed (${response.status}): ${message}`);
148
+ }
149
+ const body = (await response.json());
150
+ const accessToken = typeof body.access_token === 'string' ? body.access_token : undefined;
151
+ const refreshToken = typeof body.refresh_token === 'string' ? body.refresh_token : undefined;
152
+ const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : Number(body.expires_in ?? 0);
153
+ if (!accessToken || !refreshToken) {
154
+ throw new Error('Login response did not contain access and refresh tokens.');
155
+ }
156
+ const expiresAt = Number.isFinite(expiresIn) && expiresIn > 0 ? Date.now() + expiresIn * 1000 : undefined;
157
+ const record = { accessToken, refreshToken, expiresAt };
158
+ const account = profileKey({ cid: options.cid, pid: options.pid, apiHost });
159
+ await this.store.set(account, record);
160
+ return record;
161
+ }
162
+ async getValidTokens(cid, pid, host, forceRealmScoped = false) {
163
+ const apiHost = normalizeApiHost(host);
164
+ const account = profileKey({ cid, pid, apiHost });
165
+ const existing = await this.store.get(account);
166
+ if (!existing) {
167
+ throw new Error(`No stored credentials for ${cid}.${pid} (${apiHost}). Run "beamo-node login" first.`);
168
+ }
169
+ // Refresh token if expired or if explicitly requested
170
+ // Note: If login was done with PID, tokens are already realm-scoped
171
+ if (forceRealmScoped || isTokenExpired(existing.expiresAt)) {
172
+ if (process.env.BEAMO_DEBUG === '1' || process.env.BEAMO_NODE_DEBUG === '1') {
173
+ console.log(`[beamo-node] Refreshing token (forceRealmScoped=${forceRealmScoped}, expired=${isTokenExpired(existing.expiresAt)})`);
174
+ }
175
+ const refreshed = await this.refreshToken(existing.refreshToken, cid, pid, apiHost);
176
+ await this.store.set(account, refreshed);
177
+ return refreshed;
178
+ }
179
+ return existing;
180
+ }
181
+ async refreshToken(refreshToken, cid, pid, apiHost) {
182
+ const scope = pid ? `${cid}.${pid}` : cid;
183
+ const response = await fetch(new URL('/basic/auth/token', apiHost), {
184
+ method: 'POST',
185
+ headers: {
186
+ Accept: 'application/json',
187
+ 'Content-Type': 'application/json',
188
+ ...(scope ? { 'X-BEAM-SCOPE': scope } : {}),
189
+ },
190
+ body: JSON.stringify({
191
+ grant_type: 'refresh_token',
192
+ refresh_token: refreshToken,
193
+ // Don't include customerScoped - refresh tokens inherit scope from the refresh token itself
194
+ // Setting X-BEAM-SCOPE header should be enough to get a realm-scoped token
195
+ }),
196
+ });
197
+ if (!response.ok) {
198
+ const message = await safeReadError(response);
199
+ throw new Error(`Failed to refresh token (${response.status}): ${message}`);
200
+ }
201
+ const body = (await response.json());
202
+ const accessToken = typeof body.access_token === 'string' ? body.access_token : undefined;
203
+ const newRefreshToken = typeof body.refresh_token === 'string' ? body.refresh_token : refreshToken;
204
+ const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : Number(body.expires_in ?? 0);
205
+ if (!accessToken) {
206
+ throw new Error('Refresh token response missing access token.');
207
+ }
208
+ const expiresAt = Number.isFinite(expiresIn) && expiresIn > 0 ? Date.now() + expiresIn * 1000 : undefined;
209
+ // Verify the refreshed token works with realm scope
210
+ if (pid && process.env.BEAMO_DEBUG === '1') {
211
+ try {
212
+ const testUrl = new URL('/basic/accounts/me', apiHost);
213
+ const testResponse = await fetch(testUrl, {
214
+ headers: {
215
+ Authorization: `Bearer ${accessToken}`,
216
+ Accept: 'application/json',
217
+ 'X-BEAM-SCOPE': scope,
218
+ },
219
+ });
220
+ if (!testResponse.ok) {
221
+ console.warn(`[beamo-node] Warning: Refreshed token validation failed (${testResponse.status}). Token may still be customer-scoped.`);
222
+ }
223
+ }
224
+ catch {
225
+ // Ignore validation errors in debug mode
226
+ }
227
+ }
228
+ return { accessToken, refreshToken: newRefreshToken, expiresAt };
229
+ }
230
+ }
231
+ function parseCommandLine(argv) {
232
+ const [command, ...rest] = argv;
233
+ return { command, args: rest };
234
+ }
235
+ function splitArgs(args, recognizedFlags) {
236
+ const flags = {};
237
+ const forward = [];
238
+ for (let index = 0; index < args.length; index += 1) {
239
+ const token = args[index];
240
+ if (!token.startsWith('-')) {
241
+ forward.push(token);
242
+ continue;
243
+ }
244
+ const key = token.replace(/^--/, '');
245
+ if (recognizedFlags.has(key)) {
246
+ const next = args[index + 1];
247
+ if (next !== undefined && !next.startsWith('-')) {
248
+ flags[key] = next;
249
+ index += 1;
250
+ }
251
+ else {
252
+ flags[key] = true;
253
+ }
254
+ }
255
+ else {
256
+ forward.push(token);
257
+ const next = args[index + 1];
258
+ if (next !== undefined && !next.startsWith('-')) {
259
+ forward.push(next);
260
+ index += 1;
261
+ }
262
+ }
263
+ }
264
+ return { flags, forward };
265
+ }
266
+ async function promptInput(question, defaultValue) {
267
+ const rl = readline.createInterface({ input: stdin, output: stdout });
268
+ return new Promise((resolve) => {
269
+ rl.question(defaultValue ? `${question} (${defaultValue}): ` : `${question}: `, (answer) => {
270
+ rl.close();
271
+ const value = answer.trim();
272
+ resolve(value === '' && defaultValue !== undefined ? defaultValue : value);
273
+ });
274
+ });
275
+ }
276
+ async function promptHidden(question) {
277
+ const rl = readline.createInterface({ input: stdin, output: stdout });
278
+ return new Promise((resolve) => {
279
+ const mutable = rl;
280
+ mutable.stdoutMuted = true;
281
+ const originalWrite = mutable._writeToOutput?.bind(mutable);
282
+ const outputStream = mutable.output ?? stdout;
283
+ mutable._writeToOutput = (stringToWrite) => {
284
+ if (mutable.stdoutMuted) {
285
+ outputStream.write('*');
286
+ }
287
+ else if (originalWrite) {
288
+ originalWrite(stringToWrite);
289
+ }
290
+ else {
291
+ outputStream.write(stringToWrite);
292
+ }
293
+ };
294
+ mutable.question(`${question}: `, (answer) => {
295
+ mutable.close();
296
+ stdout.write(os.EOL);
297
+ resolve(answer);
298
+ });
299
+ });
300
+ }
301
+ async function handleLogin(args, configStore, tokenManager) {
302
+ const { flags } = splitArgs(args, new Set(['cid', 'pid', 'host', 'email']));
303
+ const envDefaults = await loadEnvDefaults();
304
+ const cid = typeof flags.cid === 'string' ? flags.cid : await promptInput('CID', envDefaults.cid);
305
+ const pid = typeof flags.pid === 'string' ? flags.pid : await promptInput('PID', envDefaults.pid);
306
+ const hostDefault = envDefaults.host ?? DEFAULT_SOCKET_HOST;
307
+ const host = typeof flags.host === 'string' ? flags.host : await promptInput('Host', hostDefault);
308
+ const email = typeof flags.email === 'string' ? flags.email : await promptInput('Email');
309
+ const password = await promptHidden('Password');
310
+ if (!cid || !pid || !host || !email || !password) {
311
+ throw new Error('CID, PID, host, email, and password are required.');
312
+ }
313
+ const record = await tokenManager.login({ cid, pid, host, email, password });
314
+ const config = await configStore.load();
315
+ const apiHost = normalizeApiHost(host);
316
+ const id = profileKey({ cid, pid, apiHost });
317
+ config.profiles[id] = {
318
+ id,
319
+ cid,
320
+ pid,
321
+ host,
322
+ apiHost,
323
+ email,
324
+ lastUsed: Date.now(),
325
+ };
326
+ config.lastUsed = id;
327
+ await configStore.save(config);
328
+ console.log(`Login successful for ${email} (${cid}.${pid}).`);
329
+ if (!record.expiresAt) {
330
+ console.warn('[beamo-node] Access token does not include an expiration. You may need to re-login if publish fails.');
331
+ }
332
+ }
333
+ async function handlePublish(args, configStore, tokenManager) {
334
+ const { flags, forward } = splitArgs(args, new Set(['cid', 'pid', 'host', 'profile']));
335
+ const profile = await resolveProfile(flags, configStore);
336
+ // Refresh token if expired (customer-scoped tokens work with registry when proper headers are used)
337
+ // The C# CLI uses the same access token for both registry API calls and Docker registry uploads
338
+ const tokens = await tokenManager.getValidTokens(profile.cid, profile.pid, profile.host, false);
339
+ const env = buildCommandEnv(profile, tokens);
340
+ const scriptArgs = buildScriptArgs(forward, profile, true); // publish needs --api-host
341
+ const scriptPath = path.resolve(__dirname, '../../scripts/publish-service.mjs');
342
+ await runScript(scriptPath, scriptArgs, env);
343
+ }
344
+ async function handleValidate(args, configStore, tokenManager) {
345
+ const { flags, forward } = splitArgs(args, new Set(['cid', 'pid', 'host', 'profile']));
346
+ const profile = await resolveProfile(flags, configStore);
347
+ // Validation does not require tokens, but downstream scripts expect CID/PID/HOST env values.
348
+ const tokens = await tokenManager.getValidTokens(profile.cid, profile.pid, profile.host);
349
+ const env = buildCommandEnv(profile, tokens);
350
+ const scriptArgs = buildScriptArgs(forward, profile, false); // validate doesn't need --api-host
351
+ const scriptPath = path.resolve(__dirname, '../../scripts/validate-service.mjs');
352
+ await runScript(scriptPath, scriptArgs, env);
353
+ }
354
+ async function resolveProfile(flags, configStore) {
355
+ const config = await configStore.load();
356
+ if (typeof flags.profile === 'string') {
357
+ const explicit = config.profiles[flags.profile];
358
+ if (!explicit) {
359
+ throw new Error(`Profile "${flags.profile}" not found. Run "beamo-node login" to create it.`);
360
+ }
361
+ config.lastUsed = explicit.id;
362
+ await configStore.save(config);
363
+ return explicit;
364
+ }
365
+ const directOptions = {
366
+ cid: typeof flags.cid === 'string' ? flags.cid : undefined,
367
+ pid: typeof flags.pid === 'string' ? flags.pid : undefined,
368
+ host: typeof flags.host === 'string' ? flags.host : undefined,
369
+ };
370
+ if (directOptions.cid && directOptions.pid && directOptions.host) {
371
+ const apiHost = normalizeApiHost(directOptions.host);
372
+ const id = profileKey({ cid: directOptions.cid, pid: directOptions.pid, apiHost });
373
+ const profile = config.profiles[id];
374
+ if (!profile) {
375
+ throw new Error(`No stored credentials for ${directOptions.cid}.${directOptions.pid}. Run "beamo-node login" first.`);
376
+ }
377
+ profile.lastUsed = Date.now();
378
+ config.lastUsed = profile.id;
379
+ await configStore.save(config);
380
+ return profile;
381
+ }
382
+ if (!config.lastUsed) {
383
+ if (Object.keys(config.profiles).length === 0) {
384
+ throw new Error('No stored credentials found. Run "beamo-node login" first.');
385
+ }
386
+ throw new Error('Multiple profiles found. Specify one with "--profile <id>" or run "beamo-node login" first.');
387
+ }
388
+ const profile = config.profiles[config.lastUsed];
389
+ if (!profile) {
390
+ throw new Error(`Last used profile "${config.lastUsed}" is missing. Run "beamo-node login" to refresh credentials.`);
391
+ }
392
+ profile.lastUsed = Date.now();
393
+ config.lastUsed = profile.id;
394
+ await configStore.save(config);
395
+ return profile;
396
+ }
397
+ function buildScriptArgs(original, profile, includeApiHost = true) {
398
+ const hasFlag = (flag) => original.some((token) => token === flag);
399
+ const args = [...original];
400
+ if (!hasFlag('--cid')) {
401
+ args.push('--cid', profile.cid);
402
+ }
403
+ if (!hasFlag('--pid')) {
404
+ args.push('--pid', profile.pid);
405
+ }
406
+ if (!hasFlag('--host')) {
407
+ args.push('--host', profile.host);
408
+ }
409
+ if (includeApiHost && !hasFlag('--api-host')) {
410
+ args.push('--api-host', profile.apiHost);
411
+ }
412
+ return args;
413
+ }
414
+ function buildCommandEnv(profile, tokens) {
415
+ return {
416
+ ...process.env,
417
+ BEAMABLE_TOKEN: tokens.accessToken,
418
+ ACCESS_TOKEN: tokens.accessToken,
419
+ BEAMABLE_REFRESH_TOKEN: tokens.refreshToken,
420
+ REFRESH_TOKEN: tokens.refreshToken,
421
+ BEAMABLE_CID: profile.cid,
422
+ CID: profile.cid,
423
+ BEAMABLE_PID: profile.pid,
424
+ PID: profile.pid,
425
+ BEAMABLE_HOST: profile.host,
426
+ HOST: profile.host,
427
+ BEAMABLE_API_HOST: profile.apiHost,
428
+ };
429
+ }
430
+ async function runScript(command, args, env) {
431
+ const isNodeScript = /\.(mjs|cjs|js)$/i.test(command);
432
+ const executable = isNodeScript ? process.execPath : command;
433
+ const finalArgs = isNodeScript ? [command, ...args] : args;
434
+ const shouldUseShell = process.platform === 'win32' && !isNodeScript;
435
+ const debug = process.env.BEAMO_DEBUG === '1' || process.env.BEAMO_NODE_DEBUG === '1';
436
+ if (debug) {
437
+ console.log(`[beamo-node] command="${command}" exec="${executable}" args=${JSON.stringify(finalArgs)} shell=${shouldUseShell} cwd=${process.cwd()}`);
438
+ }
439
+ await new Promise((resolve, reject) => {
440
+ const child = spawn(executable, finalArgs, {
441
+ stdio: 'inherit',
442
+ env,
443
+ shell: shouldUseShell,
444
+ });
445
+ child.on('error', (error) => reject(error));
446
+ child.on('exit', (code) => {
447
+ if (code === 0) {
448
+ resolve();
449
+ }
450
+ else {
451
+ reject(new Error(`Command failed with exit code ${code ?? 'unknown'}.`));
452
+ }
453
+ });
454
+ });
455
+ }
456
+ function normalizeApiHost(host) {
457
+ const trimmed = host.trim();
458
+ if (trimmed.startsWith('wss://')) {
459
+ return `https://${trimmed.substring('wss://'.length).replace(/\/socket$/, '')}`;
460
+ }
461
+ if (trimmed.startsWith('ws://')) {
462
+ return `http://${trimmed.substring('ws://'.length).replace(/\/socket$/, '')}`;
463
+ }
464
+ let normalized = trimmed.replace(/\/socket$/, '').replace(/\/$/, '');
465
+ if (!/^https?:\/\//i.test(normalized)) {
466
+ normalized = `https://${normalized}`;
467
+ }
468
+ return normalized;
469
+ }
470
+ function profileKey(input) {
471
+ return `${input.cid}.${input.pid}@${input.apiHost}`;
472
+ }
473
+ function isTokenExpired(expiresAt) {
474
+ if (!expiresAt || !Number.isFinite(expiresAt)) {
475
+ return true;
476
+ }
477
+ return Date.now() + MIN_TOKEN_BUFFER_MS >= expiresAt;
478
+ }
479
+ async function safeReadError(response) {
480
+ try {
481
+ const body = await response.text();
482
+ if (!body) {
483
+ return 'No response body';
484
+ }
485
+ try {
486
+ const parsed = JSON.parse(body);
487
+ return typeof parsed.error === 'string'
488
+ ? parsed.error
489
+ : typeof parsed.message === 'string'
490
+ ? parsed.message
491
+ : body;
492
+ }
493
+ catch {
494
+ return body;
495
+ }
496
+ }
497
+ catch {
498
+ return 'No response body';
499
+ }
500
+ }
501
+ async function main() {
502
+ const [, , ...argv] = process.argv;
503
+ const { command, args } = parseCommandLine(argv);
504
+ if (!command || command === '--help' || command === '-h') {
505
+ printHelp();
506
+ return;
507
+ }
508
+ const credentialStore = await CredentialStore.create();
509
+ const tokenManager = new TokenManager(credentialStore);
510
+ const configStore = new ConfigStore();
511
+ switch (command) {
512
+ case 'login':
513
+ await handleLogin(args, configStore, tokenManager);
514
+ break;
515
+ case 'publish':
516
+ await handlePublish(args, configStore, tokenManager);
517
+ break;
518
+ case 'validate':
519
+ await handleValidate(args, configStore, tokenManager);
520
+ break;
521
+ case 'profiles':
522
+ await listProfiles(configStore);
523
+ break;
524
+ default:
525
+ console.error(`Unknown command: ${command}`);
526
+ printHelp();
527
+ exit(1);
528
+ }
529
+ }
530
+ async function listProfiles(configStore) {
531
+ const config = await configStore.load();
532
+ const entries = Object.values(config.profiles).sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0));
533
+ if (entries.length === 0) {
534
+ console.log('No stored profiles. Run "beamo-node login" to create one.');
535
+ return;
536
+ }
537
+ console.log('Stored profiles:');
538
+ for (const profile of entries) {
539
+ const marker = config.lastUsed === profile.id ? '*' : ' ';
540
+ console.log(` ${marker} ${profile.id}`);
541
+ console.log(` CID: ${profile.cid}`);
542
+ console.log(` PID: ${profile.pid}`);
543
+ console.log(` Host: ${profile.host}`);
544
+ console.log(` Email: ${profile.email}`);
545
+ }
546
+ }
547
+ function printHelp() {
548
+ console.log(`beamo-node CLI
549
+
550
+ Usage:
551
+ beamo-node login [--cid CID] [--pid PID] [--host HOST] [--email EMAIL]
552
+ beamo-node publish [--profile ID] [--cid CID --pid PID --host HOST] [-- ...publish flags...]
553
+ beamo-node validate [--profile ID] [--cid CID --pid PID --host HOST] [-- ...validate flags...]
554
+ beamo-node profiles
555
+
556
+ Commands:
557
+ login Authenticate with Beamable and store credentials securely.
558
+ publish Run the publish pipeline using stored credentials (forwards extra flags to the publish script).
559
+ validate Generate OpenAPI docs and validate the project (forwards extra flags).
560
+ profiles List stored profiles and indicate the default selection.
561
+ `);
562
+ }
563
+ main().catch((error) => {
564
+ console.error(error instanceof Error ? error.message : error);
565
+ exit(1);
566
+ });
567
+ async function loadEnvDefaults() {
568
+ const envPath = path.resolve('.env');
569
+ try {
570
+ const content = await fs.readFile(envPath, 'utf8');
571
+ const parsed = dotenv.parse(content);
572
+ const cid = parsed.BEAMABLE_CID ?? parsed.CID ?? undefined;
573
+ const pid = parsed.BEAMABLE_PID ?? parsed.PID ?? undefined;
574
+ const host = parsed.BEAMABLE_HOST ?? parsed.HOST ?? undefined;
575
+ return {
576
+ cid: cid?.trim() || undefined,
577
+ pid: pid?.trim() || undefined,
578
+ host: host?.trim() || undefined,
579
+ };
580
+ }
581
+ catch (error) {
582
+ if (error.code === 'ENOENT') {
583
+ return {};
584
+ }
585
+ throw error;
586
+ }
587
+ }
588
+ //# sourceMappingURL=index.js.map