runcloud 0.1.104 → 0.1.105

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/diagnose.js CHANGED
@@ -3,6 +3,28 @@ import { promisify } from 'node:util';
3
3
  import { GoogleAuth, Impersonated } from 'google-auth-library';
4
4
  import { renderSessionDiagnosticTerminal, writeSessionDiagnosticBundle, } from './sessionDiagnostics.js';
5
5
  const execFileAsync = promisify(execFile);
6
+ const AUTH_ATTEMPT_TIMEOUT_MS = 10_000;
7
+ const AUTH_DISCOVERY_PROJECT_ID = 'newly-diagnostics';
8
+ async function withDeadline(label, operation) {
9
+ const controller = new AbortController();
10
+ let timer;
11
+ try {
12
+ return await Promise.race([
13
+ operation(controller.signal),
14
+ new Promise((_, reject) => {
15
+ timer = setTimeout(() => {
16
+ controller.abort();
17
+ reject(new Error(`${label} timed out after ${AUTH_ATTEMPT_TIMEOUT_MS}ms`));
18
+ }, AUTH_ATTEMPT_TIMEOUT_MS);
19
+ timer.unref?.();
20
+ }),
21
+ ]);
22
+ }
23
+ finally {
24
+ if (timer)
25
+ clearTimeout(timer);
26
+ }
27
+ }
6
28
  const DEFAULT_URLS = {
7
29
  dev: {
8
30
  apiOps: 'https://cp-api-ops-dev-97683904813.us-east4.run.app',
@@ -102,26 +124,36 @@ function exactAudienceAuthorization(authorization, audience) {
102
124
  return authorization;
103
125
  }
104
126
  async function impersonatedIdentityToken(audience, serviceAccount) {
105
- const sourceClient = await new GoogleAuth({
106
- scopes: ['https://www.googleapis.com/auth/cloud-platform'],
107
- }).getClient();
108
- const impersonated = new Impersonated({
109
- sourceClient,
110
- targetPrincipal: serviceAccount,
111
- targetScopes: ['https://www.googleapis.com/auth/cloud-platform'],
112
- lifetime: 600,
127
+ return withDeadline('service-account impersonation', async (signal) => {
128
+ const sourceClient = await new GoogleAuth({
129
+ projectId: AUTH_DISCOVERY_PROJECT_ID,
130
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
131
+ clientOptions: { transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS } },
132
+ }).getClient();
133
+ const impersonated = new Impersonated({
134
+ sourceClient,
135
+ targetPrincipal: serviceAccount,
136
+ targetScopes: ['https://www.googleapis.com/auth/cloud-platform'],
137
+ lifetime: 600,
138
+ transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS },
139
+ });
140
+ const token = await impersonated.fetchIdToken(audience, { includeEmail: true });
141
+ return exactAudienceAuthorization(`Bearer ${token}`, audience);
113
142
  });
114
- const token = await impersonated.fetchIdToken(audience, { includeEmail: true });
115
- return exactAudienceAuthorization(`Bearer ${token}`, audience);
116
143
  }
117
144
  export async function resolveOpsAuthHeader(env = process.env, requestedAudience, requestedServiceAccount) {
118
145
  const audience = requestedAudience ?? env.DIAGNOSE_OIDC_AUDIENCE ?? 'newly-diagnose-ops';
119
146
  const serviceAccount = env.DIAGNOSE_OPS_IMPERSONATE_SERVICE_ACCOUNT ?? requestedServiceAccount;
120
147
  let adcError = 'unknown ADC error';
121
148
  try {
122
- const auth = new GoogleAuth();
123
- const client = await auth.getIdTokenClient(audience);
124
- return exactAudienceAuthorization(extractAuthorization(await client.getRequestHeaders()), audience);
149
+ return await withDeadline('ADC ID-token resolution', async (signal) => {
150
+ const auth = new GoogleAuth({
151
+ projectId: AUTH_DISCOVERY_PROJECT_ID,
152
+ clientOptions: { transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS } },
153
+ });
154
+ const client = await auth.getIdTokenClient(audience);
155
+ return exactAudienceAuthorization(extractAuthorization(await client.getRequestHeaders()), audience);
156
+ });
125
157
  }
126
158
  catch (error) {
127
159
  adcError = error instanceof Error ? error.message : String(error);
@@ -251,6 +283,8 @@ export async function fetchSessionDiagnosticReport(input) {
251
283
  params.set('session', input.session);
252
284
  if (input.project)
253
285
  params.set('project', projectIdFromInput(input.project));
286
+ if (input.user)
287
+ params.set('user', input.user);
254
288
  if (input.at)
255
289
  params.set('at', input.at);
256
290
  const res = await fetch(`${input.target.url}/diagnose/session?${params}`, {
@@ -293,17 +327,18 @@ export function registerDiagnose(program) {
293
327
  .option('--env <env>', 'dev, prod, or pr-<number>', 'dev')
294
328
  .option('--session <id>', 'diagnose this exact session')
295
329
  .option('--project <id-or-url>', 'diagnose the latest/nearest session for a project')
296
- .option('--at <iso-time>', 'select the project session nearest this incident time')
330
+ .option('--user <email>', 'diagnose the latest/nearest session started by this user')
331
+ .option('--at <iso-time>', 'select the project/user session nearest this incident time')
297
332
  .option('--out <dir>', 'write evidence, report, diagram, and agent repro/fix prompt')
298
333
  .option('--json', 'emit the merged JSON document instead of the human rollup')
299
334
  .action(async (opts) => {
300
335
  try {
301
- if (opts.session || opts.project) {
302
- if (Boolean(opts.session) === Boolean(opts.project)) {
303
- throw new Error('Provide exactly one of --session or --project');
336
+ if (opts.session || opts.project || opts.user) {
337
+ if ([opts.session, opts.project, opts.user].filter(Boolean).length !== 1) {
338
+ throw new Error('Provide exactly one of --session, --project, or --user');
304
339
  }
305
- if (opts.at && !opts.project) {
306
- throw new Error('--at requires --project because an exact --session needs no time selection');
340
+ if (opts.at && opts.session) {
341
+ throw new Error('--at requires --project or --user because an exact --session needs no time selection');
307
342
  }
308
343
  if (opts.at && Number.isNaN(Date.parse(opts.at))) {
309
344
  throw new Error('--at must be an ISO timestamp');
@@ -315,6 +350,7 @@ export function registerDiagnose(program) {
315
350
  authHeader,
316
351
  ...(opts.session ? { session: opts.session } : {}),
317
352
  ...(opts.project ? { project: opts.project } : {}),
353
+ ...(opts.user ? { user: opts.user } : {}),
318
354
  ...(opts.at ? { at: opts.at } : {}),
319
355
  });
320
356
  if (opts.out) {
@@ -326,7 +362,7 @@ export function registerDiagnose(program) {
326
362
  return;
327
363
  }
328
364
  if (opts.at || opts.out) {
329
- throw new Error('--at and --out require --session or --project');
365
+ throw new Error('--at and --out require --session, --project, or --user');
330
366
  }
331
367
  if (opts.env !== 'dev' && opts.env !== 'prod') {
332
368
  throw new Error('Global health diagnosis supports only --env dev or prod');
@@ -24,7 +24,16 @@ function nodeLabel(report, id) {
24
24
  return `[${STATUS_TEXT[item.status]} ${item.label}]`;
25
25
  }
26
26
  export function renderAsciiDiagram(report) {
27
- const main = ['session', 'venue', 'runner', 'command', 'metro', 'preview'];
27
+ const main = [
28
+ 'session',
29
+ 'venue',
30
+ 'runner',
31
+ 'command',
32
+ 'metro',
33
+ 'simulator_lease',
34
+ 'simulator_app',
35
+ 'preview',
36
+ ];
28
37
  const mainLine = main.map((id) => nodeLabel(report, id)).join(' -> ');
29
38
  const runnerOffset = main.slice(0, 3).map((id) => nodeLabel(report, id)).join(' -> ').length;
30
39
  const editor = nodeLabel(report, 'code_server');
@@ -42,7 +51,7 @@ function mermaidEscape(value) {
42
51
  export function renderMermaidDiagram(report) {
43
52
  const lines = [
44
53
  'flowchart LR',
45
- ' session --> venue --> runner --> command --> metro --> preview',
54
+ ' session --> venue --> runner --> command --> metro --> simulator_lease --> simulator_app --> preview',
46
55
  ' runner --> code_server',
47
56
  ];
48
57
  for (const item of report.stages) {
@@ -172,6 +181,8 @@ Use the adjacent \`bundle.json\` as evidence, not as a conclusion to repeat blin
172
181
 
173
182
  If the diagnosis is \`evidence_missing\`, repair evidence collection first: verify the \`session_diagnostic_events\` migration is applied, API/worker/launcher provenance matches, and the controlled PR preview smoke persists its full correlated timeline. Do not infer a product cause from absent telemetry.
174
183
 
184
+ For \`simulator_app_failed\`, inspect the persisted \`simulator.app.failed\` event first. In a live newly-desktop cloud session, call \`get_mobile_preview_logs\` to retrieve the retained native startup tail, Metro output, debugger status, and React Native log registry. Treat application exceptions as userland code to reproduce and fix in the project; do not relabel a successful \`simctl\`/\`adb\` launch as a healthy app.
185
+
175
186
  1. Identify the first boundary whose recorded output does not match its required input.
176
187
  2. Add or extend the smallest regression test that reproduces that exact failure. For cross-service delivery, use the normal PR environment smoke/e2e path instead of mocks alone.
177
188
  3. Fix the production path durably in \`control-plane/\`, \`newly-desktop/\`, or \`newly-cli/\`. Do not add work to the frozen legacy Modal backend or Neon stack.
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.104';
1
+ export const CLI_VERSION = '0.1.105';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [