runcloud 0.1.5 → 0.1.6

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/api.js CHANGED
@@ -9,7 +9,8 @@ export class ApiError extends Error {
9
9
  }
10
10
  function parsedErrorDetail(body) {
11
11
  try {
12
- const detail = JSON.parse(body).detail;
12
+ const parsed = JSON.parse(body);
13
+ const detail = parsed.detail ?? parsed.error ?? parsed.message;
13
14
  if (typeof detail === 'string')
14
15
  return detail;
15
16
  if (detail !== undefined)
@@ -161,8 +162,10 @@ function detailSuffix(err) {
161
162
  }
162
163
  function headline(err) {
163
164
  if (err instanceof ApiError) {
164
- if (err.status === 401)
165
- return 'Session expired or invalid — run `newly login`.';
165
+ if (err.status === 401) {
166
+ const command = (process.argv[1] ?? '').includes('run-cloud') ? 'runcloud login' : 'newly login';
167
+ return `Session expired or invalid — run \`${command}\`.`;
168
+ }
166
169
  if (err.status === 403)
167
170
  return err.detail ? `Forbidden: ${err.detail}` : 'Forbidden.';
168
171
  if (err.status === 404)
@@ -15,6 +15,7 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
15
15
  'Access-Control-Allow-Origin': appOrigin,
16
16
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
17
17
  'Access-Control-Allow-Headers': 'content-type',
18
+ 'Access-Control-Allow-Private-Network': 'true',
18
19
  };
19
20
  const server = http.createServer((req, res) => {
20
21
  if (req.method === 'OPTIONS') {
@@ -37,6 +38,22 @@ export async function startCallbackServer(state, appOrigin, timeoutMs) {
37
38
  res.end('state mismatch');
38
39
  return;
39
40
  }
41
+ if (typeof parsed.token !== 'string' ||
42
+ parsed.token.length === 0 ||
43
+ typeof parsed.expiresIn !== 'number' ||
44
+ !Number.isFinite(parsed.expiresIn) ||
45
+ parsed.expiresIn <= 0 ||
46
+ typeof parsed.apiUrl !== 'string') {
47
+ res.writeHead(400, corsHeaders);
48
+ res.end('invalid credential payload');
49
+ return;
50
+ }
51
+ const apiUrl = new URL(parsed.apiUrl);
52
+ if (!['http:', 'https:'].includes(apiUrl.protocol)) {
53
+ res.writeHead(400, corsHeaders);
54
+ res.end('invalid API URL');
55
+ return;
56
+ }
40
57
  res.writeHead(200, { ...corsHeaders, 'Content-Type': 'application/json' });
41
58
  res.end(JSON.stringify({ ok: true }));
42
59
  clearTimeout(timer);
@@ -74,6 +91,12 @@ function openBrowser(url) {
74
91
  });
75
92
  child.unref();
76
93
  }
94
+ export function cliAuthUrl(appUrl, port, state) {
95
+ const url = new URL('/cli-auth', appUrl);
96
+ url.searchParams.set('port', String(port));
97
+ url.searchParams.set('state', state);
98
+ return url.toString();
99
+ }
77
100
  export function parseManualCredential(blob) {
78
101
  let parsed;
79
102
  try {
@@ -90,12 +113,13 @@ export function parseManualCredential(blob) {
90
113
  return { token: c.token, expiresIn: c.expiresIn, apiUrl: c.apiUrl };
91
114
  }
92
115
  export function registerLogin(program) {
93
- const defaultAppUrl = program.name() === 'runcloud'
116
+ const isRunCloud = program.name() === 'runcloud';
117
+ const defaultAppUrl = isRunCloud
94
118
  ? process.env.RUN_CLOUD_APP_URL ?? 'https://run.cloud'
95
119
  : process.env.NEWLY_APP_URL ?? 'https://newly.app';
96
120
  program
97
121
  .command('login')
98
- .description('Log in with your Newly account via the browser')
122
+ .description(`Log in with your ${isRunCloud ? 'run.cloud' : 'Newly'} account via the browser`)
99
123
  .option('--app-url <url>', 'product web app URL', defaultAppUrl)
100
124
  .option('--manual', 'paste the credential from the browser instead of using the local callback', false)
101
125
  .option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
@@ -105,36 +129,57 @@ export function registerLogin(program) {
105
129
  try {
106
130
  const hasApiUrl = typeof opts.apiUrl === 'string' && opts.apiUrl.length > 0;
107
131
  const hasToken = typeof opts.token === 'string' && opts.token.length > 0;
108
- if (hasApiUrl !== hasToken) {
132
+ if (!isRunCloud && hasApiUrl !== hasToken) {
109
133
  throw new Error('--api-url and --token must be provided together for headless login.');
110
134
  }
111
- if (hasApiUrl && hasToken) {
135
+ if (isRunCloud && hasApiUrl && !hasToken) {
136
+ throw new Error('--api-url requires --token for headless login.');
137
+ }
138
+ if (hasToken) {
139
+ if (isRunCloud && !opts.token.startsWith('rc_live_')) {
140
+ throw new Error('runcloud login requires a Better Auth API key beginning with rc_live_.');
141
+ }
142
+ const apiUrl = isRunCloud
143
+ ? opts.apiUrl ?? process.env.RUN_CLOUD_API_URL ?? 'https://api.newly.app'
144
+ : opts.apiUrl;
112
145
  try {
113
- new URL(opts.apiUrl);
146
+ new URL(apiUrl);
114
147
  }
115
148
  catch {
116
- throw new Error(`--api-url is not a valid URL: ${opts.apiUrl}`);
149
+ throw new Error(`--api-url is not a valid URL: ${apiUrl}`);
117
150
  }
118
151
  saveCredentials({
119
152
  token: opts.token,
120
- apiUrl: opts.apiUrl,
153
+ apiUrl,
121
154
  appUrl: opts.appUrl,
122
- expiresAt: Math.floor(Date.now() / 1000) + opts.expiresIn,
155
+ expiresAt: isRunCloud
156
+ ? Number.MAX_SAFE_INTEGER
157
+ : Math.floor(Date.now() / 1000) + opts.expiresIn,
123
158
  });
124
- console.log(`Logged in headlessly. Credentials written for API ${opts.apiUrl}.`);
159
+ console.log(`Logged in headlessly. Credentials written for API ${apiUrl}.`);
125
160
  return;
126
161
  }
127
162
  if (opts.manual) {
163
+ if (isRunCloud) {
164
+ const manualUrl = `${opts.appUrl.replace(/\/$/, '')}/cli-auth?manual=1`;
165
+ console.log(`Opening your browser to authorize the CLI…\n ${manualUrl}\n`);
166
+ openBrowser(manualUrl);
167
+ }
128
168
  const { createInterface } = await import('node:readline/promises');
129
169
  const rl = createInterface({ input: process.stdin, output: process.stdout });
130
170
  const blob = await rl.question('Paste the credential from the browser: ');
131
171
  rl.close();
132
172
  const parsed = parseManualCredential(blob);
173
+ if (isRunCloud && !parsed.token.startsWith('rc_live_')) {
174
+ throw new Error('The browser did not return a Better Auth run.cloud API key.');
175
+ }
133
176
  saveCredentials({
134
177
  token: parsed.token,
135
178
  apiUrl: parsed.apiUrl,
136
179
  appUrl: opts.appUrl,
137
- expiresAt: Math.floor(Date.now() / 1000) + parsed.expiresIn,
180
+ expiresAt: isRunCloud
181
+ ? Number.MAX_SAFE_INTEGER
182
+ : Math.floor(Date.now() / 1000) + parsed.expiresIn,
138
183
  });
139
184
  console.log('Logged in.');
140
185
  return;
@@ -142,17 +187,22 @@ export function registerLogin(program) {
142
187
  const state = crypto.randomBytes(16).toString('hex');
143
188
  const appOrigin = new URL(opts.appUrl).origin;
144
189
  const { port, waitForToken, close } = await startCallbackServer(state, appOrigin, LOGIN_TIMEOUT_MS);
145
- const loginUrl = `${opts.appUrl}/cli-auth?port=${port}&state=${state}`;
190
+ const loginUrl = cliAuthUrl(opts.appUrl, port, state);
146
191
  console.log(`Opening your browser to authorize the CLI…\n ${loginUrl}\n`);
147
192
  console.log("If the browser doesn't open, paste the URL above into it manually.");
148
193
  openBrowser(loginUrl);
149
194
  try {
150
195
  const payload = await waitForToken();
196
+ if (isRunCloud && !payload.token.startsWith('rc_live_')) {
197
+ throw new Error('The browser did not return a Better Auth run.cloud API key.');
198
+ }
151
199
  saveCredentials({
152
200
  token: payload.token,
153
201
  apiUrl: payload.apiUrl,
154
202
  appUrl: opts.appUrl,
155
- expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
203
+ expiresAt: isRunCloud
204
+ ? Number.MAX_SAFE_INTEGER
205
+ : Math.floor(Date.now() / 1000) + payload.expiresIn,
156
206
  });
157
207
  console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
158
208
  }
@@ -1,37 +1,8 @@
1
- import { friendlyApiError } from '../api.js';
2
- class CpClient {
3
- baseUrl;
4
- token;
5
- constructor(baseUrl, token) {
6
- this.baseUrl = baseUrl;
7
- this.token = token;
8
- }
9
- async request(method, path, body) {
10
- const res = await fetch(`${this.baseUrl}${path}`, {
11
- method,
12
- headers: { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json' },
13
- body: body !== undefined ? JSON.stringify(body) : undefined,
14
- });
15
- const text = await res.text();
16
- if (!res.ok) {
17
- let msg = text;
18
- try {
19
- msg = JSON.parse(text).error ?? text;
20
- }
21
- catch {
22
- }
23
- throw new Error(`control plane ${res.status}: ${msg}`);
24
- }
25
- return text ? JSON.parse(text) : null;
26
- }
27
- }
28
- function cp(opts) {
29
- const baseUrl = (opts.cpUrl || process.env.SBX_API_URL || 'http://127.0.0.1:8080').replace(/\/+$/, '');
30
- const token = opts.cpToken || process.env.SBX_TOKEN;
31
- if (!token) {
32
- throw new Error('No control-plane token: set SBX_TOKEN or pass --cp-token.');
33
- }
34
- return new CpClient(baseUrl, token);
1
+ import { ApiClient, friendlyApiError } from '../api.js';
2
+ import { requireCredentials } from '../config.js';
3
+ function runCloudApi() {
4
+ const credentials = requireCredentials();
5
+ return new ApiClient(credentials.apiUrl, credentials.token);
35
6
  }
36
7
  function print(value, opts) {
37
8
  if (opts.json) {
@@ -62,39 +33,39 @@ async function run(fn) {
62
33
  process.exitCode = 1;
63
34
  }
64
35
  }
65
- function withCp(cmd) {
66
- return cmd
67
- .option('--cp-url <url>', 'control-plane base URL (default $SBX_API_URL or http://127.0.0.1:8080)')
68
- .option('--cp-token <token>', 'control-plane org token (or $SBX_TOKEN; required)')
69
- .option('--json', 'output JSON', false);
36
+ function withOutput(cmd) {
37
+ return cmd.option('--json', 'output JSON', false);
70
38
  }
71
39
  export function registerSandbox(program) {
72
40
  const sandbox = program.command('sandbox').description('Spawn and control microVM sandboxes');
73
- withCp(sandbox
41
+ withOutput(sandbox
74
42
  .command('create')
75
43
  .description('Spawn a microVM sandbox')
76
44
  .option('--image <ref>', 'base image', 'newly/agent-base')
77
45
  .option('--size <class>', 'size preset', 's-2')
78
46
  .option('--region <region>', 'placement region')
79
- .option('--name <name>', 'human-readable name')).action((opts) => run(async () => {
80
- const body = { size_class: opts.size, image: opts.image };
47
+ .option('--name <name>', 'human-readable name')
48
+ .option('--org <id>', 'billing and ownership organization')).action((opts) => run(async () => {
49
+ const body = { size: opts.size, image: opts.image };
81
50
  if (opts.name)
82
51
  body.name = opts.name;
83
52
  if (opts.region)
84
53
  body.region = opts.region;
85
- print(await cp(opts).request('POST', '/v1/sandboxes', body), opts);
54
+ if (opts.org)
55
+ body.orgId = opts.org;
56
+ print(await runCloudApi().post('/run-cloud/sandboxes', body), opts);
86
57
  }));
87
- withCp(sandbox.command('list').description('List sandboxes').option('--state <state>', 'filter by state')).action((opts) => run(async () => {
58
+ withOutput(sandbox.command('list').description('List sandboxes').option('--state <state>', 'filter by state')).action((opts) => run(async () => {
88
59
  const qs = opts.state ? `?state=${encodeURIComponent(opts.state)}` : '';
89
- const data = await cp(opts).request('GET', `/v1/sandboxes${qs}`);
60
+ const data = await runCloudApi().get(`/run-cloud/sandboxes${qs}`);
90
61
  print(data?.items ?? [], opts);
91
62
  }));
92
- withCp(sandbox
63
+ withOutput(sandbox
93
64
  .command('exec')
94
65
  .description('Run a command in a sandbox (via /bin/sh -c)')
95
66
  .argument('<id>', 'sandbox id')
96
67
  .argument('<cmd...>', 'command to run, e.g. npm run build')).action((id, cmdParts, opts) => run(async () => {
97
- const r = await cp(opts).request('POST', `/v1/sandboxes/${encodeURIComponent(id)}/exec`, {
68
+ const r = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/exec`, {
98
69
  cmd: ['/bin/sh', '-c', cmdParts.join(' ')],
99
70
  });
100
71
  if (opts.json) {
@@ -108,13 +79,13 @@ export function registerSandbox(program) {
108
79
  }
109
80
  process.exitCode = r.exit_code ?? 0;
110
81
  }));
111
- withCp(sandbox
82
+ withOutput(sandbox
112
83
  .command('logs')
113
84
  .description('Show recent sandbox logs (one-shot journalctl tail; streaming is a follow-up)')
114
85
  .argument('<id>', 'sandbox id')
115
86
  .option('--lines <n>', 'number of lines to tail', '200')).action((id, opts) => run(async () => {
116
87
  const n = Number(opts.lines) || 200;
117
- const r = await cp(opts).request('POST', `/v1/sandboxes/${encodeURIComponent(id)}/exec`, {
88
+ const r = await runCloudApi().post(`/run-cloud/sandboxes/${encodeURIComponent(id)}/exec`, {
118
89
  cmd: [
119
90
  '/bin/sh',
120
91
  '-c',
@@ -128,8 +99,8 @@ export function registerSandbox(program) {
128
99
  process.stdout.write(r.stdout ?? '');
129
100
  process.stderr.write(r.stderr ?? '');
130
101
  }));
131
- withCp(sandbox.command('rm').description('Destroy a sandbox').argument('<id>', 'sandbox id')).action((id, opts) => run(async () => {
132
- await cp(opts).request('DELETE', `/v1/sandboxes/${encodeURIComponent(id)}`);
102
+ withOutput(sandbox.command('rm').description('Destroy a sandbox').argument('<id>', 'sandbox id')).action((id, opts) => run(async () => {
103
+ await runCloudApi().delete(`/run-cloud/sandboxes/${encodeURIComponent(id)}`);
133
104
  if (!opts.json)
134
105
  console.log(`destroyed ${id}`);
135
106
  }));
package/dist/config.js CHANGED
@@ -41,10 +41,10 @@ export function loadCredentials() {
41
41
  const envApiUrl = isRunCloud()
42
42
  ? process.env.RUN_CLOUD_API_URL
43
43
  : process.env.NEWLY_API_URL;
44
- if (envToken?.trim() && envApiUrl?.trim()) {
44
+ if (envToken?.trim() && (isRunCloud() || envApiUrl?.trim())) {
45
45
  return {
46
46
  token: envToken.trim(),
47
- apiUrl: normalizeApiUrl(envApiUrl.trim()),
47
+ apiUrl: normalizeApiUrl(envApiUrl?.trim() || 'https://api.newly.app'),
48
48
  appUrl: isRunCloud()
49
49
  ? process.env.RUN_CLOUD_APP_URL ?? 'https://run.cloud'
50
50
  : process.env.NEWLY_APP_URL ?? 'https://newly.app',
@@ -105,6 +105,10 @@ export function requireCredentials() {
105
105
  if (!creds) {
106
106
  throw new Error(`Not logged in (or session expired). Run: ${loginCommandName()}`);
107
107
  }
108
+ if (isRunCloud() && !creds.token.startsWith('rc_live_')) {
109
+ throw new Error('Legacy run.cloud credentials are no longer supported. Run `runcloud login` to sign in ' +
110
+ 'with Better Auth.');
111
+ }
108
112
  return creds;
109
113
  }
110
114
  export function writeProjectLink(dir, link) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Create and control run.cloud remote mobile simulator sessions",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",