@newly/cli 0.1.91 → 0.1.93

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/README.md CHANGED
@@ -29,10 +29,28 @@ newly email status # inspect Newly Email setup
29
29
  ```bash
30
30
  newly login # opens a browser, stores a session token in ~/.newly/credentials
31
31
  newly login --manual # paste the credential instead of using the browser callback
32
+ newly whoami # show the stored credential: account, API, expiry, file
32
33
  newly logout # delete stored credentials
33
34
  ```
34
35
 
35
- The token expires. Re-run `newly login` when commands report you're logged out.
36
+ The token expires. Re-run `newly login` when commands report you're logged out —
37
+ it reports the existing session first and offers to keep it, so running it out of
38
+ habit no longer throws away a working credential.
39
+
40
+ `newly whoami` reads the stored credential without calling the API, and exits
41
+ non-zero when there is none or it has expired:
42
+
43
+ ```
44
+ Account you@example.com
45
+ API https://api.newly.app
46
+ Expires 8/30/2026, 5:02:58 PM
47
+ Credential ~/.newly/credentials
48
+ ```
49
+
50
+ It reports whatever would actually be sent, including a credential supplied
51
+ through `NEWLY_API_TOKEN`, which takes precedence over the saved file. The
52
+ account line is only filled in when the sign-in method revealed an address; the
53
+ browser handoff never sees one.
36
54
 
37
55
  ## Project linking
38
56
 
@@ -28,6 +28,7 @@ curl -fsSL https://run.cloud/install | sh
28
28
 
29
29
  ```bash
30
30
  runcloud login # sign in with email, Google, or GitHub; stores a token in ~/.run-cloud/credentials
31
+ runcloud whoami # show the stored credential: account, API, expiry, file
31
32
  runcloud logout # delete the stored credential
32
33
  runcloud account # show your entitlement and metered balance
33
34
  ```
@@ -37,6 +38,23 @@ show a short code to confirm, so login works over SSH and on headless machines t
37
38
  pick with `--email`, `--google`, `--github`, or `--browser` to skip the prompt.
38
39
 
39
40
  The token expires — re-run `runcloud login` when a command reports you're logged out.
41
+ Logging in while a valid session exists reports that session and offers to keep it,
42
+ so running it out of habit no longer replaces a working credential.
43
+
44
+ `runcloud whoami` answers which credential is in play without calling the API, and
45
+ exits non-zero when there is none or it has expired:
46
+
47
+ ```
48
+ Account you@example.com
49
+ API https://api.run.cloud
50
+ Expires never
51
+ Credential ~/.run-cloud/credentials
52
+ ```
53
+
54
+ It reports whatever would actually be sent, including a credential supplied through
55
+ `RUN_CLOUD_API_KEY`, which takes precedence over the saved file. The account line is
56
+ only filled in when the sign-in method revealed an address — email sign-in does, the
57
+ browser and device handoffs do not.
40
58
 
41
59
  ## Remote simulators
42
60
 
@@ -3,7 +3,7 @@ import crypto from 'node:crypto';
3
3
  import { hostname } from 'node:os';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { ApiClient, ApiError } from '../api.js';
6
- import { clearCredentials, credentialsDisplayPath, loadCredentials, normalizeApiUrl, saveCredentials, } from '../config.js';
6
+ import { clearCredentials, credentialsDisplayPath, loadCredentials, loginCommandName, normalizeApiUrl, sanitizeAccount, saveCredentials, } from '../config.js';
7
7
  import { acquirePromptIO, ask, PromptAbortedError, select, } from '../prompt.js';
8
8
  import { AuthError, createApiKey, fetchProviders, isValidEmail, isValidOtp, normalizeOtp, sendEmailCode, verifyEmailCode, } from '../runCloudAuth.js';
9
9
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
@@ -91,7 +91,13 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
91
91
  res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
92
92
  res.end(JSON.stringify({ ok: true }));
93
93
  clearTimeout(timer);
94
- resolvePayload(parsed);
94
+ const account = sanitizeAccount(parsed.account);
95
+ resolvePayload({
96
+ token: parsed.token,
97
+ expiresIn: parsed.expiresIn,
98
+ apiUrl: parsed.apiUrl,
99
+ ...(account ? { account } : {}),
100
+ });
95
101
  }
96
102
  catch {
97
103
  res.writeHead(400, corsHeaders);
@@ -186,7 +192,13 @@ export function parseManualCredential(blob) {
186
192
  throw new Error('Pasted credential is missing required fields (expected JSON with token, expiresIn, apiUrl). ' +
187
193
  'Copy the entire blob shown in the browser.');
188
194
  }
189
- return { token: c.token, expiresIn: c.expiresIn, apiUrl: c.apiUrl };
195
+ const account = sanitizeAccount(c.account);
196
+ return {
197
+ token: c.token,
198
+ expiresIn: c.expiresIn,
199
+ apiUrl: c.apiUrl,
200
+ ...(account ? { account } : {}),
201
+ };
190
202
  }
191
203
  export async function verifyRunCloudCredential(apiUrl, token) {
192
204
  try {
@@ -215,12 +227,25 @@ export async function revokeRunCloudCredential(apiUrl, token) {
215
227
  'revoke it from the run.cloud dashboard.');
216
228
  }
217
229
  }
230
+ function expiresAtLabel(creds) {
231
+ if (creds.expiresAt >= Number.MAX_SAFE_INTEGER)
232
+ return null;
233
+ return new Date(creds.expiresAt * 1000).toLocaleString();
234
+ }
218
235
  export function existingLoginSummary(creds) {
219
236
  const who = creds.account ? ` as ${creds.account}` : '';
220
- const expiry = creds.expiresAt < Number.MAX_SAFE_INTEGER
221
- ? `, valid until ${new Date(creds.expiresAt * 1000).toLocaleString()}`
222
- : '';
223
- return `You are already logged in${who} (${creds.apiUrl}${expiry}).`;
237
+ const expires = expiresAtLabel(creds);
238
+ return `You are already logged in${who} (${creds.apiUrl}${expires ? `, valid until ${expires}` : ''}).`;
239
+ }
240
+ export function whoamiReport(creds) {
241
+ const rows = [
242
+ ['Account', creds.account ?? 'not recorded (browser sign-in never sees an address)'],
243
+ ['API', creds.apiUrl],
244
+ ['Expires', expiresAtLabel(creds) ?? 'never'],
245
+ ['Credential', credentialsDisplayPath()],
246
+ ];
247
+ const width = Math.max(...rows.map(([label]) => label.length));
248
+ return rows.map(([label, value]) => `${label.padEnd(width + 2)}${value}`).join('\n');
224
249
  }
225
250
  export async function confirmReplaceExistingLogin(creds) {
226
251
  if (!creds)
@@ -305,6 +330,7 @@ export function registerLogin(program) {
305
330
  apiUrl: parsed.apiUrl,
306
331
  appUrl: opts.appUrl,
307
332
  expiresAt: Math.floor(Date.now() / 1000) + parsed.expiresIn,
333
+ ...(parsed.account ? { account: parsed.account } : {}),
308
334
  });
309
335
  console.log('Logged in.');
310
336
  return;
@@ -367,6 +393,7 @@ export function registerLogin(program) {
367
393
  appUrl: opts.appUrl,
368
394
  expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
369
395
  revocable: true,
396
+ ...(payload.account ? { account: payload.account } : {}),
370
397
  });
371
398
  console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
372
399
  return;
@@ -387,8 +414,10 @@ export function registerLogin(program) {
387
414
  apiUrl: payload.apiUrl,
388
415
  appUrl: opts.appUrl,
389
416
  expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
417
+ ...(payload.account ? { account: payload.account } : {}),
390
418
  });
391
- console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
419
+ console.log(`Logged in${payload.account ? ` as ${payload.account}` : ''}. ` +
420
+ `Credentials saved to ${credentialsDisplayPath()}`);
392
421
  }
393
422
  finally {
394
423
  close();
@@ -404,6 +433,20 @@ export function registerLogin(program) {
404
433
  process.exitCode = 1;
405
434
  }
406
435
  });
436
+ program
437
+ .command('whoami')
438
+ .description(isRunCloud
439
+ ? 'Show which run.cloud account this machine is signed in as'
440
+ : 'Show which Newly account this machine is signed in as')
441
+ .action(() => {
442
+ const creds = loadCredentials();
443
+ if (!creds) {
444
+ console.error(`Not logged in. Run \`${loginCommandName()}\`.`);
445
+ process.exitCode = 1;
446
+ return;
447
+ }
448
+ console.log(whoamiReport(creds));
449
+ });
407
450
  program
408
451
  .command('logout')
409
452
  .description(isRunCloud
package/dist/config.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, join } from 'node:path';
4
+ const MAX_ACCOUNT_LENGTH = 320;
5
+ export function sanitizeAccount(value) {
6
+ if (typeof value !== 'string')
7
+ return undefined;
8
+ const trimmed = value.trim();
9
+ if (trimmed.length === 0 || trimmed.length > MAX_ACCOUNT_LENGTH)
10
+ return undefined;
11
+ return trimmed;
12
+ }
4
13
  export function isRunCloud() {
5
14
  const names = [process.argv[1], process.execPath].map((path) => basename(path ?? ''));
6
15
  return names.some((name) => name === 'runcloud' || name === 'run-cloud' || name === 'run-cloud.js');
@@ -1,4 +1,4 @@
1
- import { normalizeApiUrl } from "./config.js";
1
+ import { 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));
@@ -32,7 +32,13 @@ export async function runDeviceLogin(deps) {
32
32
  });
33
33
  if (res.ok) {
34
34
  const body = (await res.json());
35
- return { token: body.token, apiUrl: body.apiUrl, expiresIn: body.expiresIn };
35
+ const account = sanitizeAccount(body.account);
36
+ return {
37
+ token: body.token,
38
+ apiUrl: body.apiUrl,
39
+ expiresIn: body.expiresIn,
40
+ ...(account ? { account } : {}),
41
+ };
36
42
  }
37
43
  const { error } = (await res.json().catch(() => ({ error: "unknown" })));
38
44
  if (error === "authorization_pending")
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.91';
1
+ export const CLI_VERSION = '0.1.93';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newly/cli",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
4
4
  "description": "Develop, provision, and deploy Newly backends from your terminal",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",