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.
- package/dist/commands/login.js +51 -194
- package/dist/deviceLogin.js +5 -4
- package/dist/index.js +2 -1
- package/dist/runCloudAuth.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -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,
|
|
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
|
|
135
|
-
const
|
|
136
|
-
|
|
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
|
|
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', '
|
|
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
|
|
323
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
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 (
|
|
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.');
|
package/dist/deviceLogin.js
CHANGED
|
@@ -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(
|
|
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(
|
|
52
|
-
throw new Error(
|
|
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(
|
|
25
|
+
.version(CLI_VERSION);
|
|
25
26
|
registerLogin(program);
|
|
26
27
|
registerStatus(program);
|
|
27
28
|
registerLink(program);
|
package/dist/runCloudAuth.js
CHANGED
|
@@ -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.
|
|
1
|
+
export const CLI_VERSION = '0.1.96';
|