runcloud 0.1.94 → 0.1.96

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.
@@ -1,13 +1,17 @@
1
- import http from 'node:http';
2
- import crypto from 'node:crypto';
3
1
  import { hostname } from 'node:os';
4
2
  import { spawn } from 'node:child_process';
5
3
  import { ApiClient, ApiError } from '../api.js';
6
- import { clearCredentials, credentialsDisplayPath, loadCredentials, loginCommandName, normalizeApiUrl, sanitizeAccount, saveCredentials, } from '../config.js';
4
+ import { clearCredentials, credentialsDisplayPath, loadCredentials, loginCommandName, normalizeApiUrl, saveCredentials, } from '../config.js';
7
5
  import { acquirePromptIO, ask, PromptAbortedError, select, } from '../prompt.js';
8
6
  import { AuthError, createApiKey, fetchProviders, isValidEmail, isValidOtp, normalizeOtp, sendEmailCode, verifyEmailCode, } from '../runCloudAuth.js';
9
- const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
10
7
  export const DEFAULT_RUN_CLOUD_API_URL = 'https://api.run.cloud';
8
+ export const DEFAULT_NEWLY_AUTH_API_URL = 'https://api.newly.app';
9
+ export function authApiUrlFor(isRunCloud, explicitApiUrl) {
10
+ if (isRunCloud) {
11
+ return explicitApiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
12
+ }
13
+ return process.env.NEWLY_AUTH_API_URL ?? DEFAULT_NEWLY_AUTH_API_URL;
14
+ }
11
15
  export function loginChoices(providers) {
12
16
  const choices = [];
13
17
  if (providers.emailOtp) {
@@ -34,92 +38,6 @@ export function methodFromFlags(flags) {
34
38
  }
35
39
  return chosen[0] ?? null;
36
40
  }
37
- export async function startCallbackServer(state, appOrigin, timeoutMs) {
38
- let resolvePayload;
39
- let rejectPayload;
40
- const payloadPromise = new Promise((resolve, reject) => {
41
- resolvePayload = resolve;
42
- rejectPayload = reject;
43
- });
44
- const timer = setTimeout(() => rejectPayload(new Error(`Login timed out after ${timeoutMs / 1000}s.\n` +
45
- "If the browser said the local handoff was blocked, Chrome gates HTTPS→localhost behind a permission: " +
46
- 'click the icon at the left of the address bar, allow "access to apps on this device", ' +
47
- 'then run login again (or use --manual).')), timeoutMs);
48
- const corsHeaders = {
49
- 'Access-Control-Allow-Origin': appOrigin,
50
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
51
- 'Access-Control-Allow-Headers': 'content-type',
52
- 'Access-Control-Allow-Private-Network': 'true',
53
- };
54
- const server = http.createServer((req, res) => {
55
- if (req.method === 'OPTIONS') {
56
- res.writeHead(204, corsHeaders);
57
- res.end();
58
- return;
59
- }
60
- if (req.method !== 'POST') {
61
- res.writeHead(405, corsHeaders);
62
- res.end();
63
- return;
64
- }
65
- let body = '';
66
- req.on('data', (chunk) => (body += chunk));
67
- req.on('end', () => {
68
- try {
69
- const parsed = JSON.parse(body);
70
- if (parsed.state !== state) {
71
- res.writeHead(403, corsHeaders);
72
- res.end('state mismatch');
73
- return;
74
- }
75
- if (typeof parsed.token !== 'string' ||
76
- parsed.token.length === 0 ||
77
- typeof parsed.expiresIn !== 'number' ||
78
- !Number.isFinite(parsed.expiresIn) ||
79
- parsed.expiresIn <= 0 ||
80
- typeof parsed.apiUrl !== 'string') {
81
- res.writeHead(400, corsHeaders);
82
- res.end('invalid credential payload');
83
- return;
84
- }
85
- const apiUrl = new URL(parsed.apiUrl);
86
- if (!['http:', 'https:'].includes(apiUrl.protocol)) {
87
- res.writeHead(400, corsHeaders);
88
- res.end('invalid API URL');
89
- return;
90
- }
91
- res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
92
- res.end(JSON.stringify({ ok: true }));
93
- clearTimeout(timer);
94
- const account = sanitizeAccount(parsed.account);
95
- resolvePayload({
96
- token: parsed.token,
97
- expiresIn: parsed.expiresIn,
98
- apiUrl: parsed.apiUrl,
99
- ...(account ? { account } : {}),
100
- });
101
- }
102
- catch {
103
- res.writeHead(400, corsHeaders);
104
- res.end('bad request');
105
- }
106
- });
107
- });
108
- const port = await new Promise((resolve) => {
109
- server.listen(0, '127.0.0.1', () => {
110
- const addr = server.address();
111
- resolve(typeof addr === 'object' && addr ? addr.port : 0);
112
- });
113
- });
114
- return {
115
- port,
116
- waitForToken: () => payloadPromise,
117
- close: () => {
118
- clearTimeout(timer);
119
- server.close();
120
- },
121
- };
122
- }
123
41
  export function openBrowser(url) {
124
42
  const [cmd, args] = process.platform === 'darwin'
125
43
  ? ['open', [url]]
@@ -131,13 +49,9 @@ export function openBrowser(url) {
131
49
  });
132
50
  child.unref();
133
51
  }
134
- export function cliAuthUrl(appUrl, port, state, provider) {
135
- const url = new URL('/cli-auth', appUrl);
136
- url.searchParams.set('port', String(port));
137
- url.searchParams.set('state', state);
138
- if (provider)
139
- url.searchParams.set('provider', provider);
140
- return url.toString();
52
+ export function commandApiUrls(apiUrl, authApiUrl) {
53
+ const same = normalizeApiUrl(apiUrl) === normalizeApiUrl(authApiUrl);
54
+ return { apiUrl, ...(same ? {} : { authApiUrl }) };
141
55
  }
142
56
  export async function emailLogin(apiUrl, presetEmail, io) {
143
57
  const email = presetEmail?.trim()
@@ -179,27 +93,6 @@ function hostLabel() {
179
93
  return 'cli';
180
94
  }
181
95
  }
182
- export function parseManualCredential(blob) {
183
- let parsed;
184
- try {
185
- parsed = JSON.parse(blob);
186
- }
187
- catch {
188
- throw new Error('Could not parse the pasted credential as JSON. Copy the entire blob shown in the browser and try again.');
189
- }
190
- const c = parsed;
191
- if (typeof c.token !== 'string' || typeof c.expiresIn !== 'number' || typeof c.apiUrl !== 'string') {
192
- throw new Error('Pasted credential is missing required fields (expected JSON with token, expiresIn, apiUrl). ' +
193
- 'Copy the entire blob shown in the browser.');
194
- }
195
- const account = sanitizeAccount(c.account);
196
- return {
197
- token: c.token,
198
- expiresIn: c.expiresIn,
199
- apiUrl: c.apiUrl,
200
- ...(account ? { account } : {}),
201
- };
202
- }
203
96
  export async function verifyRunCloudCredential(apiUrl, token) {
204
97
  try {
205
98
  await new ApiClient(normalizeApiUrl(apiUrl), token).get('/run-cloud/account');
@@ -241,6 +134,7 @@ export function whoamiReport(creds) {
241
134
  const rows = [
242
135
  ['Account', creds.account ?? 'not recorded (browser sign-in never sees an address)'],
243
136
  ['API', creds.apiUrl],
137
+ ...(creds.authApiUrl ? [['Issued by', creds.authApiUrl]] : []),
244
138
  ['Expires', expiresAtLabel(creds) ?? 'never'],
245
139
  ['Credential', credentialsDisplayPath()],
246
140
  ];
@@ -274,9 +168,9 @@ export function registerLogin(program) {
274
168
  .command('login')
275
169
  .description(isRunCloud
276
170
  ? 'Log in with your run.cloud account: email, Google, or GitHub'
277
- : 'Log in with your Newly account via the browser')
171
+ : 'Log in with your Newly account: email, Google, or GitHub')
278
172
  .option('--app-url <url>', 'product web app URL', defaultAppUrl)
279
- .option('--manual', 'newly skin only: paste the credential from the browser instead of using the local callback', false)
173
+ .option('--manual', 'deprecated no-op: the browser→localhost paste fallback is gone (device flow works headless)', false)
280
174
  .option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
281
175
  .option('--token <token>', 'explicit credential token (skips browser; use with --api-url)')
282
176
  .option('--expires-in <seconds>', 'session lifetime when using --token (default 7 days)', (v) => Number(v), 604800)
@@ -319,30 +213,18 @@ export function registerLogin(program) {
319
213
  console.log(` Keeping the credential at ${credentialsDisplayPath()}\n`);
320
214
  return;
321
215
  }
322
- if (opts.manual && !isRunCloud) {
323
- const { createInterface } = await import('node:readline/promises');
324
- const rl = createInterface({ input: process.stdin, output: process.stdout });
325
- const blob = await rl.question('Paste the credential from the browser: ');
326
- rl.close();
327
- const parsed = parseManualCredential(blob);
328
- saveCredentials({
329
- token: parsed.token,
330
- apiUrl: parsed.apiUrl,
331
- appUrl: opts.appUrl,
332
- expiresAt: Math.floor(Date.now() / 1000) + parsed.expiresIn,
333
- ...(parsed.account ? { account: parsed.account } : {}),
334
- });
335
- console.log('Logged in.');
336
- return;
216
+ if (opts.manual) {
217
+ console.log('Note: --manual is a no-op now — sign-in never posts to localhost, so it works over SSH and headless already.');
337
218
  }
219
+ const authApiUrl = authApiUrlFor(isRunCloud, opts.apiUrl);
338
220
  let socialProvider;
339
- if (isRunCloud && !opts.browser) {
340
- const authApiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
221
+ if (!opts.browser) {
341
222
  let method = methodFromFlags(opts);
342
223
  const terminal = method === 'email' || !method ? acquirePromptIO() : null;
343
224
  try {
225
+ let providers = null;
344
226
  if (!method && terminal) {
345
- const providers = await fetchProviders(authApiUrl);
227
+ providers = await fetchProviders(authApiUrl);
346
228
  const choices = loginChoices(providers);
347
229
  if (choices.length === 0) {
348
230
  throw new Error(`${authApiUrl} has no sign-in methods enabled.`);
@@ -351,20 +233,27 @@ export function registerLogin(program) {
351
233
  }
352
234
  if (method === 'email') {
353
235
  const preset = typeof opts.email === 'string' ? opts.email : undefined;
236
+ providers ??= await fetchProviders(authApiUrl);
354
237
  const { email, token } = await emailLogin(authApiUrl, preset, terminal?.io);
355
238
  saveCredentials({
356
239
  token,
357
- apiUrl: authApiUrl,
358
240
  appUrl: opts.appUrl,
359
241
  expiresAt: Number.MAX_SAFE_INTEGER,
360
242
  revocable: true,
361
243
  account: email,
244
+ ...commandApiUrls(providers.cliApiUrl ?? authApiUrl, authApiUrl),
362
245
  });
363
246
  console.log(`\n Signed in as ${email}`);
364
247
  console.log(` Credential saved to ${credentialsDisplayPath()}\n`);
365
248
  console.log(' Try:');
366
- console.log(' runcloud ios create --open');
367
- console.log(' runcloud sandbox run "echo hello"');
249
+ if (isRunCloud) {
250
+ console.log(' runcloud ios create --open');
251
+ console.log(' runcloud sandbox run "echo hello"');
252
+ }
253
+ else {
254
+ console.log(' newly whoami');
255
+ console.log(' newly status');
256
+ }
368
257
  return;
369
258
  }
370
259
  }
@@ -374,54 +263,24 @@ export function registerLogin(program) {
374
263
  if (method === 'google' || method === 'github')
375
264
  socialProvider = method;
376
265
  }
377
- if (isRunCloud) {
378
- if (opts.manual) {
379
- console.log('Note: --manual is no longer needed for run.cloud — `runcloud login` works over SSH and headless via device flow.');
380
- }
381
- const apiUrl = opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? DEFAULT_RUN_CLOUD_API_URL;
382
- const { runDeviceLogin } = await import('../deviceLogin.js');
383
- const payload = await runDeviceLogin({
384
- apiUrl,
385
- appUrl: opts.appUrl,
386
- provider: socialProvider,
387
- openBrowser,
388
- });
389
- await verifyRunCloudCredential(payload.apiUrl, payload.token);
390
- saveCredentials({
391
- token: payload.token,
392
- apiUrl: payload.apiUrl,
393
- appUrl: opts.appUrl,
394
- expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
395
- revocable: true,
396
- ...(payload.account ? { account: payload.account } : {}),
397
- });
398
- console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
399
- return;
400
- }
401
- const state = crypto.randomBytes(16).toString('hex');
402
- const appOrigin = new URL(opts.appUrl).origin;
403
- const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
404
- const loginUrl = cliAuthUrl(opts.appUrl, port, state, socialProvider);
405
- console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
406
- console.log("If the browser doesn't open, paste the URL above into it manually.");
407
- openBrowser(loginUrl);
408
- try {
409
- const payload = await waitForToken();
410
- if (isRunCloud)
411
- await verifyRunCloudCredential(payload.apiUrl, payload.token);
412
- saveCredentials({
413
- token: payload.token,
414
- apiUrl: payload.apiUrl,
415
- appUrl: opts.appUrl,
416
- expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
417
- ...(payload.account ? { account: payload.account } : {}),
418
- });
419
- console.log(`Logged in${payload.account ? ` as ${payload.account}` : ''}. ` +
420
- `Credentials saved to ${credentialsDisplayPath()}`);
421
- }
422
- finally {
423
- close();
424
- }
266
+ const { runDeviceLogin } = await import('../deviceLogin.js');
267
+ const payload = await runDeviceLogin({
268
+ apiUrl: authApiUrl,
269
+ appUrl: opts.appUrl,
270
+ provider: socialProvider,
271
+ openBrowser,
272
+ });
273
+ await verifyRunCloudCredential(authApiUrl, payload.token);
274
+ saveCredentials({
275
+ token: payload.token,
276
+ appUrl: opts.appUrl,
277
+ expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
278
+ revocable: true,
279
+ ...(payload.account ? { account: payload.account } : {}),
280
+ ...commandApiUrls(payload.apiUrl, authApiUrl),
281
+ });
282
+ console.log(`Logged in${payload.account ? ` as ${payload.account}` : ''}. ` +
283
+ `Credentials saved to ${credentialsDisplayPath()}`);
425
284
  }
426
285
  catch (e) {
427
286
  if (e instanceof PromptAbortedError) {
@@ -449,13 +308,11 @@ export function registerLogin(program) {
449
308
  });
450
309
  program
451
310
  .command('logout')
452
- .description(isRunCloud
453
- ? 'Revoke this machine’s credential and delete it locally'
454
- : 'Delete the stored CLI credential')
311
+ .description('Revoke this machine’s credential and delete it locally')
455
312
  .action(async () => {
456
313
  const creds = loadCredentials();
457
- if (isRunCloud && creds?.revocable) {
458
- console.log(await revokeRunCloudCredential(creds.apiUrl, creds.token));
314
+ if (creds?.revocable) {
315
+ console.log(await revokeRunCloudCredential(creds.authApiUrl ?? creds.apiUrl, creds.token));
459
316
  }
460
317
  clearCredentials();
461
318
  console.log('Logged out.');
@@ -1,10 +1,11 @@
1
- import { normalizeApiUrl, sanitizeAccount } from "./config.js";
1
+ import { loginCommandName, normalizeApiUrl, sanitizeAccount } from "./config.js";
2
2
  export async function runDeviceLogin(deps) {
3
3
  const fetchFn = deps.fetchFn ?? fetch;
4
4
  const log = deps.log ?? ((m) => console.log(m));
5
5
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6
6
  const now = deps.now ?? (() => Date.now());
7
7
  const base = normalizeApiUrl(deps.apiUrl);
8
+ const retry = loginCommandName();
8
9
  const startRes = await fetchFn(`${base}/run-cloud/cli-auth/device`, {
9
10
  method: "POST",
10
11
  headers: { "content-type": "application/json" },
@@ -23,7 +24,7 @@ export async function runDeviceLogin(deps) {
23
24
  const deadline = now() + start.expires_in * 1000;
24
25
  for (;;) {
25
26
  if (now() >= deadline)
26
- throw new Error("Login timed out. Run `runcloud login` again.");
27
+ throw new Error(`Login timed out. Run \`${retry}\` again.`);
27
28
  await sleep(intervalMs);
28
29
  const res = await fetchFn(`${base}/run-cloud/cli-auth/token`, {
29
30
  method: "POST",
@@ -48,7 +49,7 @@ export async function runDeviceLogin(deps) {
48
49
  continue;
49
50
  }
50
51
  if (error === "access_denied")
51
- throw new Error("Authorization was denied. Run `runcloud login` again.");
52
- throw new Error("The login code expired. Run `runcloud login` again.");
52
+ throw new Error(`Authorization was denied. Run \`${retry}\` again.`);
53
+ throw new Error(`The login code expired. Run \`${retry}\` again.`);
53
54
  }
54
55
  }
package/dist/index.js CHANGED
@@ -19,9 +19,10 @@ import { registerDiagnose } from './diagnose.js';
19
19
  import { registerDesktop } from './commands/desktop.js';
20
20
  import { registerEnvironment } from './commands/environment.js';
21
21
  import { registerAppStore } from './commands/appStore.js';
22
+ import { CLI_VERSION } from './version.js';
22
23
  const program = new Command('newly')
23
24
  .description('Develop, provision, and deploy Newly backends')
24
- .version('0.1.0');
25
+ .version(CLI_VERSION);
25
26
  registerLogin(program);
26
27
  registerStatus(program);
27
28
  registerLink(program);
@@ -41,11 +41,13 @@ export async function fetchProviders(apiUrl) {
41
41
  if (!response.ok)
42
42
  throw new AuthError(await readError(response), response.status);
43
43
  const payload = (await response.json());
44
+ const cliApiUrl = typeof payload.cliApiUrl === 'string' ? payload.cliApiUrl.trim() : '';
44
45
  return {
45
46
  emailOtp: payload.emailOtp === true,
46
47
  google: payload.google === true,
47
48
  github: payload.github === true,
48
49
  apple: payload.apple === true,
50
+ ...(cliApiUrl ? { cliApiUrl } : {}),
49
51
  };
50
52
  }
51
53
  export async function sendEmailCode(apiUrl, email) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.94';
1
+ export const CLI_VERSION = '0.1.96';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.94",
3
+ "version": "0.1.96",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [