@run-cloud/sdk 0.19.0 → 0.21.0

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/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import WebSocket from "ws";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
+ import { RemotionClient } from "./remotion.js";
4
+ export * from "./remotion.js";
3
5
  export const SIMULATOR_KEYS = [
4
6
  "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
5
7
  "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
@@ -74,7 +76,18 @@ export const SIMULATOR_SCREENSHOT_ERROR_CODES = [
74
76
  "screenshot_invalid_response",
75
77
  "screenshot_failed",
76
78
  ];
79
+ export const SIMULATOR_ACCESSIBILITY_ERROR_CODES = [
80
+ "active_session_not_found",
81
+ "simulator_capacity_unavailable",
82
+ "simulator_session_ended",
83
+ "accessibility_unavailable",
84
+ "accessibility_cancelled",
85
+ "accessibility_timeout",
86
+ "accessibility_transport_error",
87
+ "accessibility_invalid_response",
88
+ ];
77
89
  export const DEFAULT_SIMULATOR_INTERACTION_TIMEOUT_MS = 15_000;
90
+ export const DEFAULT_SIMULATOR_ACCESSIBILITY_TIMEOUT_MS = 20_000;
78
91
  const SIMULATOR_RESPONSE_MARGIN_MS = 250;
79
92
  const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
80
93
  function assertNormalizedPoint(point, name) {
@@ -235,6 +248,188 @@ export function validateSimulatorInteraction(interaction, timeoutMs = DEFAULT_SI
235
248
  return;
236
249
  }
237
250
  }
251
+ const simulatorAccessibilityRoleSet = new Set([
252
+ "application", "window", "dialog", "alert", "group", "scroll-view", "list",
253
+ "list-item", "text", "heading", "button", "link", "image", "text-field",
254
+ "checkbox", "switch", "radio-button", "slider", "progress-indicator", "tab",
255
+ "tab-bar", "toolbar", "menu", "menu-item", "web-view", "keyboard", "other",
256
+ ]);
257
+ function accessibilityRecord(value, field) {
258
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
259
+ throw new TypeError(`${field} must be an object`);
260
+ }
261
+ return value;
262
+ }
263
+ function accessibilityString(value, field) {
264
+ if (typeof value !== "string" || value.length === 0 || value.length > 16_384) {
265
+ throw new TypeError(`${field} must be a non-empty bounded string`);
266
+ }
267
+ return value;
268
+ }
269
+ function accessibilityNullableString(value, field) {
270
+ if (value === null)
271
+ return null;
272
+ if (typeof value !== "string" || value.length > 16_384) {
273
+ throw new TypeError(`${field} must be null or a bounded string`);
274
+ }
275
+ return value;
276
+ }
277
+ function accessibilityBoolean(value, field) {
278
+ if (typeof value !== "boolean")
279
+ throw new TypeError(`${field} must be a boolean`);
280
+ return value;
281
+ }
282
+ function accessibilityNullableBoolean(value, field) {
283
+ if (value !== null && typeof value !== "boolean") {
284
+ throw new TypeError(`${field} must be a boolean or null`);
285
+ }
286
+ return value;
287
+ }
288
+ function accessibilityNumber(value, field, nonNegative = false) {
289
+ if (typeof value !== "number" || !Number.isFinite(value) || (nonNegative && value < 0)) {
290
+ throw new TypeError(`${field} must be a${nonNegative ? " non-negative" : ""} finite number`);
291
+ }
292
+ return value;
293
+ }
294
+ function parseAccessibilityBounds(value, field) {
295
+ if (value === null)
296
+ return null;
297
+ const input = accessibilityRecord(value, field);
298
+ return {
299
+ x: accessibilityNumber(input.x, `${field}.x`),
300
+ y: accessibilityNumber(input.y, `${field}.y`),
301
+ width: accessibilityNumber(input.width, `${field}.width`, true),
302
+ height: accessibilityNumber(input.height, `${field}.height`, true),
303
+ };
304
+ }
305
+ function parseAccessibilityStates(value, field) {
306
+ const input = accessibilityRecord(value, field);
307
+ if (input.checked !== null && typeof input.checked !== "boolean" && input.checked !== "mixed") {
308
+ throw new TypeError(`${field}.checked must be a boolean, mixed, or null`);
309
+ }
310
+ return {
311
+ enabled: accessibilityBoolean(input.enabled, `${field}.enabled`),
312
+ focused: accessibilityNullableBoolean(input.focused, `${field}.focused`),
313
+ selected: accessibilityNullableBoolean(input.selected, `${field}.selected`),
314
+ checked: input.checked,
315
+ expanded: accessibilityNullableBoolean(input.expanded, `${field}.expanded`),
316
+ focusable: accessibilityNullableBoolean(input.focusable, `${field}.focusable`),
317
+ clickable: accessibilityNullableBoolean(input.clickable, `${field}.clickable`),
318
+ scrollable: accessibilityNullableBoolean(input.scrollable, `${field}.scrollable`),
319
+ editable: accessibilityBoolean(input.editable, `${field}.editable`),
320
+ secure: accessibilityBoolean(input.secure, `${field}.secure`),
321
+ };
322
+ }
323
+ function parseAccessibilityNative(value, platform, field) {
324
+ const input = accessibilityRecord(value, field);
325
+ if (input.platform !== platform)
326
+ throw new TypeError(`${field}.platform must match the snapshot`);
327
+ if (platform === "ios") {
328
+ return {
329
+ platform,
330
+ type: accessibilityNullableString(input.type, `${field}.type`),
331
+ roleDescription: accessibilityNullableString(input.roleDescription, `${field}.roleDescription`),
332
+ subrole: accessibilityNullableString(input.subrole, `${field}.subrole`),
333
+ title: accessibilityNullableString(input.title, `${field}.title`),
334
+ help: accessibilityNullableString(input.help, `${field}.help`),
335
+ };
336
+ }
337
+ return {
338
+ platform,
339
+ className: accessibilityNullableString(input.className, `${field}.className`),
340
+ packageName: accessibilityNullableString(input.packageName, `${field}.packageName`),
341
+ resourceId: accessibilityNullableString(input.resourceId, `${field}.resourceId`),
342
+ contentDescription: accessibilityNullableString(input.contentDescription, `${field}.contentDescription`),
343
+ checkable: accessibilityBoolean(input.checkable, `${field}.checkable`),
344
+ longClickable: accessibilityBoolean(input.longClickable, `${field}.longClickable`),
345
+ password: accessibilityBoolean(input.password, `${field}.password`),
346
+ };
347
+ }
348
+ function parseAccessibilityNode(value, expectedPath, depth, context) {
349
+ if (depth > 80 || context.count >= 500) {
350
+ throw new TypeError("accessibility tree exceeds its declared bounds");
351
+ }
352
+ const field = `accessibility node ${expectedPath}`;
353
+ const input = accessibilityRecord(value, field);
354
+ const id = accessibilityString(input.id, `${field}.id`);
355
+ const path = accessibilityString(input.path, `${field}.path`);
356
+ if (path !== expectedPath)
357
+ throw new TypeError(`${field}.path must be ${expectedPath}`);
358
+ if (context.ids.has(id))
359
+ throw new TypeError(`duplicate accessibility node id: ${id}`);
360
+ context.ids.add(id);
361
+ context.count += 1;
362
+ if (typeof input.role !== "string"
363
+ || !simulatorAccessibilityRoleSet.has(input.role))
364
+ throw new TypeError(`${field}.role is not supported`);
365
+ const role = input.role;
366
+ const states = parseAccessibilityStates(input.states, `${field}.states`);
367
+ const native = parseAccessibilityNative(input.native, context.platform, `${field}.native`);
368
+ if (!Array.isArray(input.children))
369
+ throw new TypeError(`${field}.children must be an array`);
370
+ const secure = states.secure
371
+ || (native.platform === "android" && native.password)
372
+ || (native.platform === "ios" && /(?:secure|password)/iu.test([
373
+ native.type,
374
+ native.roleDescription,
375
+ native.subrole,
376
+ ].filter(Boolean).join(" ")));
377
+ return {
378
+ id,
379
+ path,
380
+ role,
381
+ label: accessibilityNullableString(input.label, `${field}.label`),
382
+ value: secure ? null : accessibilityNullableString(input.value, `${field}.value`),
383
+ identifier: accessibilityNullableString(input.identifier, `${field}.identifier`),
384
+ bounds: parseAccessibilityBounds(input.bounds, `${field}.bounds`),
385
+ states: { ...states, secure },
386
+ native,
387
+ children: input.children.map((child, index) => (parseAccessibilityNode(child, `${expectedPath}.${index}`, depth + 1, context))),
388
+ };
389
+ }
390
+ /** Validate an API response and remove any secure value defensively. */
391
+ export function validateSimulatorAccessibilitySnapshot(value, expectedPlatform) {
392
+ const input = accessibilityRecord(value, "accessibility snapshot");
393
+ if (input.schemaVersion !== 1)
394
+ throw new TypeError("accessibility schemaVersion must be 1");
395
+ if (input.platform !== expectedPlatform)
396
+ throw new TypeError("accessibility platform mismatch");
397
+ const capturedAt = accessibilityString(input.capturedAt, "accessibility capturedAt");
398
+ if (!Number.isFinite(Date.parse(capturedAt))) {
399
+ throw new TypeError("accessibility capturedAt must be an ISO date-time string");
400
+ }
401
+ const screenInput = accessibilityRecord(input.screen, "accessibility screen");
402
+ const unit = expectedPlatform === "ios" ? "points" : "pixels";
403
+ if (screenInput.unit !== unit)
404
+ throw new TypeError(`accessibility screen.unit must be ${unit}`);
405
+ const nodeCount = accessibilityNumber(input.nodeCount, "accessibility nodeCount", true);
406
+ if (!Number.isInteger(nodeCount) || nodeCount > 500) {
407
+ throw new TypeError("accessibility nodeCount must be an integer from 0 to 500");
408
+ }
409
+ if (!Array.isArray(input.roots))
410
+ throw new TypeError("accessibility roots must be an array");
411
+ const context = {
412
+ platform: expectedPlatform,
413
+ ids: new Set(),
414
+ count: 0,
415
+ };
416
+ const roots = input.roots.map((root, index) => (parseAccessibilityNode(root, String(index), 0, context)));
417
+ if (context.count !== nodeCount)
418
+ throw new TypeError("accessibility nodeCount does not match tree");
419
+ return {
420
+ schemaVersion: 1,
421
+ platform: expectedPlatform,
422
+ capturedAt,
423
+ screen: {
424
+ width: accessibilityNumber(screenInput.width, "accessibility screen.width", true),
425
+ height: accessibilityNumber(screenInput.height, "accessibility screen.height", true),
426
+ unit,
427
+ },
428
+ nodeCount,
429
+ truncated: accessibilityBoolean(input.truncated, "accessibility truncated"),
430
+ roots,
431
+ };
432
+ }
238
433
  function requestAbort(options) {
239
434
  const { signal, timeoutMs } = options;
240
435
  if (timeoutMs === undefined && !signal) {
@@ -372,6 +567,7 @@ function optionalFiniteNumber(value) {
372
567
  }
373
568
  const simulatorInteractionErrorCodeSet = new Set(SIMULATOR_INTERACTION_ERROR_CODES);
374
569
  const simulatorScreenshotErrorCodeSet = new Set(SIMULATOR_SCREENSHOT_ERROR_CODES);
570
+ const simulatorAccessibilityErrorCodeSet = new Set(SIMULATOR_ACCESSIBILITY_ERROR_CODES);
375
571
  function normalizedInteractionErrorCode(value) {
376
572
  if (typeof value === "string" && simulatorInteractionErrorCodeSet.has(value)) {
377
573
  return value;
@@ -402,6 +598,11 @@ function normalizedScreenshotErrorCode(value) {
402
598
  ? value
403
599
  : "screenshot_failed";
404
600
  }
601
+ function normalizedAccessibilityErrorCode(value) {
602
+ return typeof value === "string" && simulatorAccessibilityErrorCodeSet.has(value)
603
+ ? value
604
+ : "accessibility_unavailable";
605
+ }
405
606
  async function parseApiError(response) {
406
607
  const text = await response.text();
407
608
  try {
@@ -428,9 +629,11 @@ async function parseApiError(response) {
428
629
  const rawCode = nested?.code ?? parsed.code;
429
630
  const code = action === "screenshot"
430
631
  ? normalizedScreenshotErrorCode(rawCode)
431
- : action !== "open-url" && (action !== undefined || interactionStatus !== undefined)
432
- ? normalizedInteractionErrorCode(rawCode)
433
- : optionalString(rawCode);
632
+ : action === "accessibility"
633
+ ? normalizedAccessibilityErrorCode(rawCode)
634
+ : action !== "open-url" && (action !== undefined || interactionStatus !== undefined)
635
+ ? normalizedInteractionErrorCode(rawCode)
636
+ : optionalString(rawCode);
434
637
  return {
435
638
  detail,
436
639
  code,
@@ -498,6 +701,7 @@ export class Client {
498
701
  secrets;
499
702
  ci;
500
703
  images;
704
+ remotion;
501
705
  apiKey;
502
706
  apiUrl;
503
707
  fetchImpl;
@@ -523,6 +727,7 @@ export class Client {
523
727
  this.secrets = new SecretsClient(this);
524
728
  this.ci = new CiClient(this);
525
729
  this.images = new ImageClient(this);
730
+ this.remotion = new RemotionClient(this.sandboxes);
526
731
  }
527
732
  async usage(options = {}) {
528
733
  const query = options.orgId ? `?orgId=${encodeURIComponent(options.orgId)}` : "";
@@ -946,6 +1151,39 @@ export class SimulatorClient {
946
1151
  async setRenderDebug(id, option, enabled, options = {}) {
947
1152
  return await this.interact(id, { action: "setRenderDebug", option, enabled }, options);
948
1153
  }
1154
+ async accessibilityTree(id, options = {}) {
1155
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SIMULATOR_ACCESSIBILITY_TIMEOUT_MS;
1156
+ assertIntegerRange(timeoutMs, 100, 60_000, "timeoutMs");
1157
+ const context = {
1158
+ sessionId: id,
1159
+ platform: this.platform,
1160
+ action: "accessibility",
1161
+ };
1162
+ const value = await this.client.request("GET", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/accessibility`, undefined, undefined, {
1163
+ signal: options.signal,
1164
+ timeoutMs: timeoutMs + SIMULATOR_RESPONSE_MARGIN_MS,
1165
+ timeoutMessage: `simulator accessibility capture did not complete within ${timeoutMs} ms`,
1166
+ timeoutCode: "accessibility_timeout",
1167
+ transportErrorCode: "accessibility_transport_error",
1168
+ transportErrorMessage: "simulator accessibility could not reach the run.cloud API",
1169
+ timeoutContext: context,
1170
+ errorContext: {
1171
+ ...context,
1172
+ code: "accessibility_unavailable",
1173
+ },
1174
+ });
1175
+ try {
1176
+ return validateSimulatorAccessibilitySnapshot(value, this.platform);
1177
+ }
1178
+ catch (error) {
1179
+ throw new RunCloudError(502, "simulator accessibility returned an invalid tree", {
1180
+ ...context,
1181
+ code: "accessibility_invalid_response",
1182
+ retryable: true,
1183
+ details: { reason: error instanceof Error ? error.message : "unknown" },
1184
+ });
1185
+ }
1186
+ }
949
1187
  async screenshot(id, options = {}) {
950
1188
  const timeoutMs = options.timeoutMs ?? DEFAULT_SIMULATOR_INTERACTION_TIMEOUT_MS;
951
1189
  const requestId = options.requestId ?? `screenshot_${randomUUID()}`;
@@ -1197,6 +1435,10 @@ export class SimulatorsClient {
1197
1435
  const { platform = "ios", ...requestOptions } = options;
1198
1436
  return await new SimulatorClient(this.client, platform).screenshot(id, requestOptions);
1199
1437
  }
1438
+ async accessibilityTree(id, options = {}) {
1439
+ const { platform = "ios", ...requestOptions } = options;
1440
+ return await new SimulatorClient(this.client, platform).accessibilityTree(id, requestOptions);
1441
+ }
1200
1442
  async startRecording(id, options = {}) {
1201
1443
  const { platform = "ios", ...requestOptions } = options;
1202
1444
  return await new SimulatorClient(this.client, platform).startRecording(id, requestOptions);