runcloud 0.2.188 → 0.2.190

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.
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { promisify } from 'node:util';
7
- import { ApiClient, friendlyApiError } from '../api.js';
7
+ import { ApiClient, ApiError, friendlyApiError } from '../api.js';
8
8
  import { requireCredentials } from '../config.js';
9
9
  const execFileAsync = promisify(execFile);
10
10
  export const DOGFOOD_SECRET_GROUP = 'newly-dogfood';
@@ -171,26 +171,57 @@ async function projectOrgId(client, projectId) {
171
171
  }
172
172
  export async function pushDogfoodCreds(client, projectId, entries) {
173
173
  const orgId = await projectOrgId(client, projectId);
174
- await client.put(`/orgs/${encodeURIComponent(orgId)}/secret-groups/${DOGFOOD_SECRET_GROUP}`, {
175
- entries,
176
- exposeToSandbox: true,
177
- force: true,
178
- });
174
+ const path = `/orgs/${encodeURIComponent(orgId)}/secret-groups/${DOGFOOD_SECRET_GROUP}`;
175
+ if (Object.keys(entries).length === 0) {
176
+ try {
177
+ await client.delete(path);
178
+ }
179
+ catch (err) {
180
+ if (!(err instanceof ApiError && err.status === 404))
181
+ throw err;
182
+ }
183
+ return;
184
+ }
185
+ await client.put(path, { entries, exposeToSandbox: true, force: true });
186
+ }
187
+ export async function fetchServerCredentials(client) {
188
+ try {
189
+ const status = await client.get('/dogfood');
190
+ const provided = status?.serverCredentials;
191
+ return { github: provided?.github === true, gcp: provided?.gcp === true };
192
+ }
193
+ catch (err) {
194
+ if (err instanceof ApiError && err.status === 404)
195
+ return { github: false, gcp: false };
196
+ throw err;
197
+ }
198
+ }
199
+ export function credentialsToSupply(server, opts) {
200
+ return { github: Boolean(opts.githubTokenFile) || !server.github, gcp: opts.gcp && !server.gcp };
179
201
  }
180
202
  async function credsOnce(opts) {
181
203
  const { client, apiUrl } = api();
182
204
  const projectId = projectIdFrom(opts.project, apiUrl);
205
+ const supply = credentialsToSupply(await fetchServerCredentials(client), opts);
206
+ if (!supply.github && !supply.gcp) {
207
+ await pushDogfoodCreds(client, projectId, {});
208
+ console.log(`${apiUrl} mints the sessions' GitHub and GCP tokens itself; cleared any stored ones.`);
209
+ return false;
210
+ }
183
211
  const entries = {};
184
- const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
185
- entries.GH_TOKEN = github.token;
186
- if (github.source === 'gh-cli') {
187
- console.log('GitHub: using your gh CLI token (all of its scopes). A fine-grained token is safer: --github-token-file.');
212
+ if (supply.github) {
213
+ const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
214
+ entries.GH_TOKEN = github.token;
215
+ if (github.source === 'gh-cli') {
216
+ console.log('GitHub: using your gh CLI token (all of its scopes). A fine-grained token is safer: --github-token-file.');
217
+ }
188
218
  }
189
- if (opts.gcp)
219
+ if (supply.gcp)
190
220
  entries.GOOGLE_OAUTH_ACCESS_TOKEN = await mintDogfoodGcpToken();
191
221
  await pushDogfoodCreds(client, projectId, entries);
192
222
  console.log(`${new Date().toISOString()} credentials pushed to project ${projectId}: ${Object.keys(entries).join(', ')}` +
193
- (opts.gcp ? ' (GCP token valid ~1h)' : ''));
223
+ (supply.gcp ? ' (GCP token valid ~1h)' : ''));
224
+ return supply.gcp;
194
225
  }
195
226
  export function dogfoodSessionBody(opts) {
196
227
  const body = {
@@ -341,25 +372,40 @@ export function registerDogfood(program) {
341
372
  .action((opts) => run(async () => {
342
373
  const { client, apiUrl } = api();
343
374
  console.log(`control plane: ${apiUrl}`);
344
- console.log('1/4 GCP: minting a dogfood-agent token by impersonation...');
345
- const gcpToken = await mintDogfoodGcpToken();
346
- console.log(' ok');
347
- console.log('2/4 SOPS: dev must decrypt, prod must be refused...');
348
- const root = await repoRoot();
349
- if (root) {
350
- const result = await verifySopsBoundary(root, gcpToken);
351
- console.log(result.detail.replace(/^/gm, ' '));
352
- if (!result.prodRefused)
353
- throw new Error('the dogfood token can decrypt prod — stop and report this');
354
- if (!result.dev)
355
- throw new Error('the dogfood token cannot decrypt dev; see the error above');
375
+ const supply = credentialsToSupply(await fetchServerCredentials(client), { githubTokenFile: opts.githubTokenFile, gcp: true });
376
+ const entries = {};
377
+ if (supply.gcp) {
378
+ console.log('1/4 GCP: minting a dogfood-agent token by impersonation...');
379
+ const gcpToken = await mintDogfoodGcpToken();
380
+ entries.GOOGLE_OAUTH_ACCESS_TOKEN = gcpToken;
381
+ console.log(' ok');
382
+ console.log('2/4 SOPS: dev must decrypt, prod must be refused...');
383
+ const root = await repoRoot();
384
+ if (root) {
385
+ const result = await verifySopsBoundary(root, gcpToken);
386
+ console.log(result.detail.replace(/^/gm, ' '));
387
+ if (!result.prodRefused)
388
+ throw new Error('the dogfood token can decrypt prod — stop and report this');
389
+ if (!result.dev)
390
+ throw new Error('the dogfood token cannot decrypt dev; see the error above');
391
+ }
392
+ else {
393
+ console.log(' skipped: run setup from a monorepo checkout to verify (needs secrets/*.yaml and sops >= 3.10)');
394
+ }
356
395
  }
357
396
  else {
358
- console.log(' skipped: run setup from a monorepo checkout to verify (needs secrets/*.yaml and sops >= 3.10)');
397
+ console.log("1/4 GCP: the control plane mints the dogfood agent's token itself (dev sops key only)");
398
+ console.log("2/4 SOPS: nothing to prove from here; the agent account's IAM is the boundary");
399
+ }
400
+ if (supply.github) {
401
+ console.log('3/4 GitHub: reading a token...');
402
+ const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
403
+ entries.GH_TOKEN = github.token;
404
+ console.log(` ok (${github.source})`);
405
+ }
406
+ else {
407
+ console.log('3/4 GitHub: the control plane gives sessions a Newly GitHub App token (the dogfood repo only)');
359
408
  }
360
- console.log('3/4 GitHub: reading a token...');
361
- const github = await readGithubToken({ tokenFile: opts.githubTokenFile });
362
- console.log(` ok (${github.source})`);
363
409
  console.log('4/4 Project...');
364
410
  let projectId = opts.project ?? (readState()?.apiUrl === apiUrl ? readState()?.projectId : undefined);
365
411
  if (!projectId) {
@@ -377,10 +423,17 @@ export function registerDogfood(program) {
377
423
  console.log(` using ${projectId}`);
378
424
  }
379
425
  writeState({ projectId, apiUrl });
380
- await pushDogfoodCreds(client, projectId, { GH_TOKEN: github.token, GOOGLE_OAUTH_ACCESS_TOKEN: gcpToken });
381
- console.log(' credentials stored as the project\'s "newly-dogfood" secret group (dogfood sessions only)');
382
- console.log('\nNext: `newly dogfood creds --watch` in a spare terminal (keeps the GCP token fresh),');
383
- console.log('then `newly dogfood start "<what to do>"`.');
426
+ await pushDogfoodCreds(client, projectId, entries);
427
+ console.log(Object.keys(entries).length > 0
428
+ ? ' credentials stored as the project\'s "newly-dogfood" secret group (dogfood sessions only)'
429
+ : ' no credentials stored: the control plane supplies them (any earlier ones were removed)');
430
+ if (supply.gcp) {
431
+ console.log('\nNext: `newly dogfood creds --watch` in a spare terminal (keeps the GCP token fresh),');
432
+ console.log('then `newly dogfood start "<what to do>"`.');
433
+ }
434
+ else {
435
+ console.log('\nNext: `newly dogfood start "<what to do>"`.');
436
+ }
384
437
  }));
385
438
  dogfood
386
439
  .command('creds')
@@ -390,8 +443,8 @@ export function registerDogfood(program) {
390
443
  .option('--no-gcp', 'do not mint a GCP token (sessions then cannot use sops)')
391
444
  .option('--watch', `keep refreshing every ${CREDS_REFRESH_MS / 60_000} minutes`, false)
392
445
  .action((opts) => run(async () => {
393
- await credsOnce(opts);
394
- if (!opts.watch)
446
+ const mintedGcp = await credsOnce(opts);
447
+ if (!opts.watch || !mintedGcp)
395
448
  return;
396
449
  for (;;) {
397
450
  await new Promise((resolve) => setTimeout(resolve, CREDS_REFRESH_MS));
package/dist/config.js CHANGED
@@ -31,6 +31,9 @@ export function credentialsDisplayPath() {
31
31
  return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
32
32
  }
33
33
  export function loginCommandName() {
34
+ const override = process.env.NEWLY_LOGIN_COMMAND?.trim();
35
+ if (override)
36
+ return override;
34
37
  return isRunCloud() ? 'runcloud login' : 'newly login';
35
38
  }
36
39
  export function normalizeApiUrl(url) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.2.188';
1
+ export const CLI_VERSION = '0.2.190';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.2.188",
3
+ "version": "0.2.190",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
Binary file