@opengeni/sdk 0.46.0 → 0.48.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/src/client.ts CHANGED
@@ -101,6 +101,8 @@ import type {
101
101
  MachinesResponse,
102
102
  MetricSample,
103
103
  MachineMetricsSeriesResponse,
104
+ RemoveEnrollmentRequest,
105
+ RemoveEnrollmentResponse,
104
106
  // Bring-your-own-compute: the user-authenticated active-sandbox swap (M7).
105
107
  SwapActiveSandboxRequest,
106
108
  SwapActiveSandboxResponse,
@@ -108,6 +110,8 @@ import type {
108
110
  PackInstallation,
109
111
  LatencyMode,
110
112
  ReasoningEffort,
113
+ RetainedScreenshotDownload,
114
+ RetainedScreenshotDownloadOptions,
111
115
  RetainedArtifactContent,
112
116
  RetainedArtifactContentOptions,
113
117
  RetainedArtifactMetadata,
@@ -159,6 +163,8 @@ import type {
159
163
  // Channel-A structured services (P4.4).
160
164
  FsListRequest,
161
165
  FsListResponse,
166
+ FsListBatchRequest,
167
+ FsListBatchResponse,
162
168
  FsReadRequest,
163
169
  FsReadResponse,
164
170
  FsWriteRequest,
@@ -173,6 +179,8 @@ import type {
173
179
  GitStatusResponse,
174
180
  GitDiffRequest,
175
181
  GitDiffResponse,
182
+ GitReadBatchRequest,
183
+ GitReadBatchResponse,
176
184
  GitLogRequest,
177
185
  GitLogResponse,
178
186
  GitShowRequest,
@@ -206,6 +214,7 @@ import type {
206
214
  SetWorkspaceDefaultRigRequest,
207
215
  UploadFileInput,
208
216
  VariableSet,
217
+ VariableSetSecret,
209
218
  VariableSetVariableMetadata,
210
219
  Rig,
211
220
  RigVersion,
@@ -264,6 +273,7 @@ import {
264
273
  OPENGENI_API_CONTRACT_HEADER,
265
274
  OPENGENI_API_CONTRACT_REVISION,
266
275
  OPENGENI_CORRELATION_HEADER,
276
+ COMPUTER_SCREENSHOT_MAX_BYTES,
267
277
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
268
278
  } from "./types";
269
279
 
@@ -363,6 +373,8 @@ export class OpenGeniClient {
363
373
  private readonly baseUrl: string;
364
374
  private readonly options: OpenGeniClientOptions;
365
375
  private readonly fetchImpl: FetchLike;
376
+ private readonly readInFlight = new Map<string, Promise<unknown>>();
377
+ private readonly readTrailing = new Map<string, Promise<unknown>>();
366
378
 
367
379
  constructor(options: OpenGeniClientOptions) {
368
380
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
@@ -385,8 +397,12 @@ export class OpenGeniClient {
385
397
  input.audio instanceof File
386
398
  ? input.audio
387
399
  : input.audio instanceof Uint8Array
388
- ? new File([Uint8Array.from(input.audio)], filename, { type: input.mimeType })
389
- : new File([input.audio], filename, { type: input.mimeType || input.audio.type });
400
+ ? new File([Uint8Array.from(input.audio)], filename, {
401
+ type: input.mimeType,
402
+ })
403
+ : new File([input.audio], filename, {
404
+ type: input.mimeType || input.audio.type,
405
+ });
390
406
  form.append("audio", audio, filename);
391
407
  form.append("mimeType", input.mimeType);
392
408
  if (input.durationSeconds !== undefined) {
@@ -396,7 +412,10 @@ export class OpenGeniClient {
396
412
  try {
397
413
  response = await this.fetchImpl(this.url(`/v1/workspaces/${workspaceId}/transcriptions`), {
398
414
  method: "POST",
399
- headers: { ...this.headers(correlationId), Accept: "application/json" },
415
+ headers: {
416
+ ...this.headers(correlationId),
417
+ Accept: "application/json",
418
+ },
400
419
  body: form,
401
420
  ...(input.signal ? { signal: input.signal } : {}),
402
421
  });
@@ -405,7 +424,11 @@ export class OpenGeniClient {
405
424
  throw mutationTransportError(correlationId);
406
425
  }
407
426
  assertApiContractResponse(response);
408
- if (!response.ok) throw await apiErrorFromResponse(response, { method: "POST", correlationId });
427
+ if (!response.ok)
428
+ throw await apiErrorFromResponse(response, {
429
+ method: "POST",
430
+ correlationId,
431
+ });
409
432
  await assertJsonResponse(response, { method: "POST", correlationId });
410
433
  let body: unknown;
411
434
  try {
@@ -509,7 +532,11 @@ export class OpenGeniClient {
509
532
  throw mutationTransportError(correlationId);
510
533
  }
511
534
  assertApiContractResponse(response);
512
- if (!response.ok) throw await apiErrorFromResponse(response, { method: "PUT", correlationId });
535
+ if (!response.ok)
536
+ throw await apiErrorFromResponse(response, {
537
+ method: "PUT",
538
+ correlationId,
539
+ });
513
540
  await assertJsonResponse(response, { method: "PUT", correlationId });
514
541
  const body = await response.json().catch(() => null);
515
542
  if (!isUploadTranscriptionRecordingChunkResponse(body)) {
@@ -603,11 +630,13 @@ export class OpenGeniClient {
603
630
  );
604
631
  }
605
632
 
606
- async getSession(workspaceId: string, sessionId: string): Promise<Session> {
607
- return await this.requestJson<Session>(
608
- "GET",
609
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}`,
610
- );
633
+ async getSession(
634
+ workspaceId: string,
635
+ sessionId: string,
636
+ options: { fresh?: boolean } = {},
637
+ ): Promise<Session> {
638
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}`;
639
+ return await this.singleFlightRead(path, () => this.requestJson<Session>("GET", path), options);
611
640
  }
612
641
 
613
642
  async updateSession(
@@ -754,12 +783,40 @@ export class OpenGeniClient {
754
783
  }
755
784
 
756
785
  async getSessionLineage(workspaceId: string, sessionId: string): Promise<SessionLineageResponse> {
757
- return await this.requestJson<SessionLineageResponse>(
758
- "GET",
759
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`,
786
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`;
787
+ return await this.singleFlightRead(path, () =>
788
+ this.requestJson<SessionLineageResponse>("GET", path),
760
789
  );
761
790
  }
762
791
 
792
+ private singleFlightRead<T>(
793
+ key: string,
794
+ read: () => Promise<T>,
795
+ options: { fresh?: boolean } = {},
796
+ ): Promise<T> {
797
+ const existing = this.readInFlight.get(key);
798
+ if (existing) {
799
+ if (!options.fresh) return existing as Promise<T>;
800
+ const queued = this.readTrailing.get(key);
801
+ if (queued) return queued as Promise<T>;
802
+ const trailing = existing.then(
803
+ () => this.singleFlightRead(key, read),
804
+ () => this.singleFlightRead(key, read),
805
+ );
806
+ this.readTrailing.set(key, trailing);
807
+ const clear = () => {
808
+ if (this.readTrailing.get(key) === trailing) this.readTrailing.delete(key);
809
+ };
810
+ void trailing.then(clear, clear);
811
+ return trailing;
812
+ }
813
+ const promise = read().finally(() => {
814
+ if (this.readInFlight.get(key) === promise) this.readInFlight.delete(key);
815
+ });
816
+ this.readInFlight.set(key, promise);
817
+ return promise;
818
+ }
819
+
763
820
  /** Negotiate one server-mediated connected-Codex GPT-Live V3 WebRTC call. */
764
821
  async negotiateCodexRealtimeWebrtc(
765
822
  workspaceId: string,
@@ -883,7 +940,9 @@ export class OpenGeniClient {
883
940
 
884
941
  /** Newest turn that durably emitted `turn.started`, or null before any admission. */
885
942
  async getLatestStartedTurn(workspaceId: string, sessionId: string): Promise<SessionTurn | null> {
886
- const turns = await this.listTurns(workspaceId, sessionId, { latestStarted: true });
943
+ const turns = await this.listTurns(workspaceId, sessionId, {
944
+ latestStarted: true,
945
+ });
887
946
  return turns[0] ?? null;
888
947
  }
889
948
 
@@ -928,6 +987,24 @@ export class OpenGeniClient {
928
987
  return response.samples;
929
988
  }
930
989
 
990
+ /**
991
+ * Remove one connected self-hosted machine enrollment. The control-plane
992
+ * operation works while the agent is offline, revokes future reconnects,
993
+ * retains history, and returns a typed blocker when active route/lease or
994
+ * recovery dependencies make removal unsafe. `idempotencyKey` is replay-safe.
995
+ */
996
+ async removeEnrollment(
997
+ workspaceId: string,
998
+ enrollmentId: string,
999
+ request: RemoveEnrollmentRequest = {},
1000
+ ): Promise<RemoveEnrollmentResponse> {
1001
+ return await this.requestJson<RemoveEnrollmentResponse>(
1002
+ "POST",
1003
+ `/v1/workspaces/${workspaceId}/enrollments/${enrollmentId}/revoke`,
1004
+ request,
1005
+ );
1006
+ }
1007
+
931
1008
  // --- Self-hosted enrollment UX (design 11) --------------------------------
932
1009
 
933
1010
  /**
@@ -1350,9 +1427,9 @@ export class OpenGeniClient {
1350
1427
  // --- Turn queue ------------------------------------------------------------
1351
1428
 
1352
1429
  async getQueue(workspaceId: string, sessionId: string): Promise<SessionQueueSnapshot> {
1353
- return await this.requestJson<SessionQueueSnapshot>(
1354
- "GET",
1355
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`,
1430
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`;
1431
+ return await this.singleFlightRead(path, () =>
1432
+ this.requestJson<SessionQueueSnapshot>("GET", path),
1356
1433
  );
1357
1434
  }
1358
1435
 
@@ -1469,7 +1546,11 @@ export class OpenGeniClient {
1469
1546
  async cancelSession(
1470
1547
  workspaceId: string,
1471
1548
  sessionId: string,
1472
- options: { reason?: string; clientEventId?: string; expectedControlEtag?: string } = {},
1549
+ options: {
1550
+ reason?: string;
1551
+ clientEventId?: string;
1552
+ expectedControlEtag?: string;
1553
+ } = {},
1473
1554
  ): Promise<SessionControlResponse> {
1474
1555
  return await this.controlSession(workspaceId, sessionId, {
1475
1556
  action: "cancel",
@@ -1614,10 +1695,8 @@ export class OpenGeniClient {
1614
1695
 
1615
1696
  /** The session's goal. 404s when the session never had one. */
1616
1697
  async getGoal(workspaceId: string, sessionId: string): Promise<SessionGoal> {
1617
- return await this.requestJson<SessionGoal>(
1618
- "GET",
1619
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`,
1620
- );
1698
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`;
1699
+ return await this.singleFlightRead(path, () => this.requestJson<SessionGoal>("GET", path));
1621
1700
  }
1622
1701
 
1623
1702
  async updateGoal(
@@ -1703,6 +1782,22 @@ export class OpenGeniClient {
1703
1782
  );
1704
1783
  }
1705
1784
 
1785
+ /** FileSystem: hydrate several independent directories behind one sandbox lease. */
1786
+ async fsListBatch(
1787
+ workspaceId: string,
1788
+ sessionId: string,
1789
+ request: FsListBatchRequest,
1790
+ options: OpenGeniRequestOptions = {},
1791
+ ): Promise<FsListBatchResponse> {
1792
+ return await this.requestJson<FsListBatchResponse>(
1793
+ "POST",
1794
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list-batch`,
1795
+ request,
1796
+ {},
1797
+ options,
1798
+ );
1799
+ }
1800
+
1706
1801
  /** FileSystem: read a file (text or base64; binary-safe, size-capped). */
1707
1802
  async fsRead(
1708
1803
  workspaceId: string,
@@ -1803,6 +1898,22 @@ export class OpenGeniClient {
1803
1898
  );
1804
1899
  }
1805
1900
 
1901
+ /** Git: read status and optional diffs for several repositories behind one sandbox lease. */
1902
+ async gitReadBatch(
1903
+ workspaceId: string,
1904
+ sessionId: string,
1905
+ request: GitReadBatchRequest,
1906
+ options: OpenGeniRequestOptions = {},
1907
+ ): Promise<GitReadBatchResponse> {
1908
+ return await this.requestJson<GitReadBatchResponse>(
1909
+ "POST",
1910
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/read-batch`,
1911
+ request,
1912
+ {},
1913
+ options,
1914
+ );
1915
+ }
1916
+
1806
1917
  /** Git: commit log. */
1807
1918
  async gitLog(
1808
1919
  workspaceId: string,
@@ -2484,7 +2595,8 @@ export class OpenGeniClient {
2484
2595
  }
2485
2596
 
2486
2597
  // --- VariableSets --------------------------------------------------------------
2487
- // Variable values are write-only: reads return name/version metadata only.
2598
+ // Generic reads return name/version metadata only. Plaintext uses one
2599
+ // dedicated permissioned endpoint.
2488
2600
 
2489
2601
  async listVariableSets(workspaceId: string): Promise<VariableSet[]> {
2490
2602
  return await this.requestJson<VariableSet[]>(
@@ -2511,6 +2623,17 @@ export class OpenGeniClient {
2511
2623
  );
2512
2624
  }
2513
2625
 
2626
+ async getVariableSetVariable(
2627
+ workspaceId: string,
2628
+ variableSetId: string,
2629
+ name: string,
2630
+ ): Promise<VariableSetSecret> {
2631
+ return await this.requestJson<VariableSetSecret>(
2632
+ "GET",
2633
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}/variables/${encodeURIComponent(name)}`,
2634
+ );
2635
+ }
2636
+
2514
2637
  async updateVariableSet(
2515
2638
  workspaceId: string,
2516
2639
  variableSetId: string,
@@ -2530,7 +2653,7 @@ export class OpenGeniClient {
2530
2653
  );
2531
2654
  }
2532
2655
 
2533
- /** Create or rotate a variable. The value never comes back on any read. */
2656
+ /** Create or rotate a variable. Generic reads never return its value. */
2534
2657
  async setVariableSetVariable(
2535
2658
  workspaceId: string,
2536
2659
  variableSetId: string,
@@ -2726,19 +2849,29 @@ export class OpenGeniClient {
2726
2849
  async beginFileUpload(
2727
2850
  workspaceId: string,
2728
2851
  request: CreateFileUploadRequest,
2852
+ options: OpenGeniRequestOptions = {},
2729
2853
  ): Promise<CreateFileUploadResponse> {
2730
2854
  return await this.requestJson<CreateFileUploadResponse>(
2731
2855
  "POST",
2732
2856
  `/v1/workspaces/${workspaceId}/files/uploads`,
2733
2857
  request,
2858
+ {},
2859
+ options,
2734
2860
  );
2735
2861
  }
2736
2862
 
2737
2863
  /** Step 3 of the upload flow: server verifies the object and marks it ready. */
2738
- async completeFileUpload(workspaceId: string, uploadId: string): Promise<FileAsset> {
2864
+ async completeFileUpload(
2865
+ workspaceId: string,
2866
+ uploadId: string,
2867
+ options: OpenGeniRequestOptions = {},
2868
+ ): Promise<FileAsset> {
2739
2869
  const response = await this.requestJson<CompleteFileUploadResponse>(
2740
2870
  "POST",
2741
2871
  `/v1/workspaces/${workspaceId}/files/uploads/${uploadId}/complete`,
2872
+ undefined,
2873
+ {},
2874
+ options,
2742
2875
  );
2743
2876
  return response.file;
2744
2877
  }
@@ -2749,6 +2882,30 @@ export class OpenGeniClient {
2749
2882
  * -> complete. Returns the ready `FileAsset`.
2750
2883
  */
2751
2884
  async uploadFile(workspaceId: string, input: UploadFileInput): Promise<FileAsset> {
2885
+ if (
2886
+ input.timeoutMs !== undefined &&
2887
+ (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0)
2888
+ ) {
2889
+ throw new Error("File upload timeout must be a positive number");
2890
+ }
2891
+ const withTimeout = async <T>(
2892
+ timeoutMs: number,
2893
+ operation: (signal: AbortSignal) => Promise<T>,
2894
+ ): Promise<T> => {
2895
+ const controller = new AbortController();
2896
+ let timeout: ReturnType<typeof setTimeout> | undefined;
2897
+ const timedOut = new Promise<never>((_resolve, reject) => {
2898
+ timeout = setTimeout(() => {
2899
+ controller.abort();
2900
+ reject(new Error("File upload timed out. Retry the upload."));
2901
+ }, timeoutMs);
2902
+ });
2903
+ try {
2904
+ return await Promise.race([operation(controller.signal), timedOut]);
2905
+ } finally {
2906
+ if (timeout !== undefined) clearTimeout(timeout);
2907
+ }
2908
+ };
2752
2909
  // Snapshot mutable inputs before hashing so the digest always describes the
2753
2910
  // exact bytes later sent to object storage. Copy Uint8Array views into a
2754
2911
  // Blob so byte offsets/shared buffers can't leak surrounding bytes.
@@ -2765,32 +2922,52 @@ export class OpenGeniClient {
2765
2922
  ? body.size
2766
2923
  : body.byteLength;
2767
2924
  const sha256 = input.sha256 ?? (await sha256ForUpload(body));
2768
- const upload = await this.beginFileUpload(workspaceId, {
2769
- filename: input.filename,
2770
- contentType: input.contentType,
2771
- sizeBytes,
2772
- sha256,
2773
- });
2774
- const putResponse = await this.fetchImpl(upload.putUrl, {
2775
- method: "PUT",
2776
- // Signed object-storage URLs carry their own short-lived authority.
2777
- // Browser cookies and HTTP auth must never accompany this cross-origin
2778
- // request: credentialed fetches are incompatible with wildcard CORS and
2779
- // can leak ambient credentials to a caller-selected storage endpoint.
2780
- credentials: "omit",
2781
- // The backend's requiredHeaders already carry the canonical lowercase
2782
- // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
2783
- // a `Content-Type` key here: WHATWG Headers treats the two casings as the
2784
- // same header and comma-joins their values (e.g. "text/plain, text/plain"),
2785
- // which the object store persists verbatim and COMPLETE then rejects (422),
2786
- // and which breaks S3's presigned-URL signature.
2787
- headers: { ...upload.requiredHeaders },
2788
- body,
2789
- });
2925
+ const upload = await withTimeout(
2926
+ 30_000,
2927
+ async (signal) =>
2928
+ await this.beginFileUpload(
2929
+ workspaceId,
2930
+ {
2931
+ filename: input.filename,
2932
+ contentType: input.contentType,
2933
+ sizeBytes,
2934
+ sha256,
2935
+ },
2936
+ { signal },
2937
+ ),
2938
+ );
2939
+ // Give large valid uploads enough time at a conservative 256 KiB/s while
2940
+ // still bounding an object-storage request that never settles.
2941
+ const transferTimeoutMs =
2942
+ input.timeoutMs ?? Math.max(120_000, Math.ceil(sizeBytes / (256 * 1024)) * 1_000);
2943
+ const putResponse = await withTimeout(
2944
+ transferTimeoutMs,
2945
+ async (signal) =>
2946
+ await this.fetchImpl(upload.putUrl, {
2947
+ method: "PUT",
2948
+ // Signed object-storage URLs carry their own short-lived authority.
2949
+ // Browser cookies and HTTP auth must never accompany this cross-origin
2950
+ // request: credentialed fetches are incompatible with wildcard CORS and
2951
+ // can leak ambient credentials to a caller-selected storage endpoint.
2952
+ credentials: "omit",
2953
+ // The backend's requiredHeaders already carry the canonical lowercase
2954
+ // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
2955
+ // a `Content-Type` key here: WHATWG Headers treats the two casings as the
2956
+ // same header and comma-joins their values (e.g. "text/plain, text/plain"),
2957
+ // which the object store persists verbatim and COMPLETE then rejects (422),
2958
+ // and which breaks S3's presigned-URL signature.
2959
+ headers: { ...upload.requiredHeaders },
2960
+ body,
2961
+ signal,
2962
+ }),
2963
+ );
2790
2964
  if (!putResponse.ok) {
2791
2965
  throw await apiErrorFromResponse(putResponse, { method: "PUT" });
2792
2966
  }
2793
- return await this.completeFileUpload(workspaceId, upload.uploadId);
2967
+ return await withTimeout(
2968
+ 30_000,
2969
+ async (signal) => await this.completeFileUpload(workspaceId, upload.uploadId, { signal }),
2970
+ );
2794
2971
  }
2795
2972
 
2796
2973
  async getFile(workspaceId: string, fileId: string): Promise<FileAsset> {
@@ -2819,23 +2996,119 @@ export class OpenGeniClient {
2819
2996
  workspaceId: string,
2820
2997
  artifactId: string,
2821
2998
  options: RetainedArtifactContentOptions = {},
2999
+ ): Promise<RetainedArtifactContent> {
3000
+ return await this.getRetainedArtifactContentAtPath(
3001
+ `/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`,
3002
+ options,
3003
+ );
3004
+ }
3005
+
3006
+ async getSessionRetainedArtifact(
3007
+ workspaceId: string,
3008
+ sessionId: string,
3009
+ artifactId: string,
3010
+ ): Promise<RetainedArtifactMetadata> {
3011
+ return await this.requestJson<RetainedArtifactMetadata>(
3012
+ "GET",
3013
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/artifacts/${artifactId}`,
3014
+ );
3015
+ }
3016
+
3017
+ async getSessionRetainedArtifactContent(
3018
+ workspaceId: string,
3019
+ sessionId: string,
3020
+ artifactId: string,
3021
+ options: RetainedArtifactContentOptions = {},
3022
+ ): Promise<RetainedArtifactContent> {
3023
+ return await this.getRetainedArtifactContentAtPath(
3024
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/artifacts/${artifactId}/content`,
3025
+ options,
3026
+ );
3027
+ }
3028
+
3029
+ /** Assemble one retained screenshot from bounded authenticated API ranges. */
3030
+ async downloadRetainedScreenshot(
3031
+ workspaceId: string,
3032
+ sessionId: string,
3033
+ artifactId: string,
3034
+ options: RetainedScreenshotDownloadOptions = {},
3035
+ ): Promise<RetainedScreenshotDownload> {
3036
+ const maxRetries = options.maxRetries ?? 2;
3037
+ if (!Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 3) {
3038
+ throw new RangeError("retained screenshot maxRetries must be an integer from 0 to 3");
3039
+ }
3040
+ const metadata = await this.getSessionRetainedArtifact(workspaceId, sessionId, artifactId);
3041
+ if (!metadata.available) return { metadata, bytes: null };
3042
+ if (
3043
+ metadata.kind !== "computer_screenshot" ||
3044
+ metadata.contentType !== "image/png" ||
3045
+ !metadata.dimensions ||
3046
+ metadata.originalBytes <= 0 ||
3047
+ metadata.originalBytes > COMPUTER_SCREENSHOT_MAX_BYTES
3048
+ ) {
3049
+ throw new OpenGeniApiError(502, "retained screenshot metadata is invalid");
3050
+ }
3051
+ const bytes = new Uint8Array(metadata.originalBytes);
3052
+ const pageBytes = Math.min(metadata.retrieval.maxRangeBytes, RETAINED_OUTPUT_MAX_PAGE_BYTES);
3053
+ if (!Number.isSafeInteger(pageBytes) || pageBytes <= 0) {
3054
+ throw new OpenGeniApiError(502, "retained screenshot range metadata is invalid");
3055
+ }
3056
+ for (let start = 0; start < bytes.byteLength; start += pageBytes) {
3057
+ options.signal?.throwIfAborted();
3058
+ const end = Math.min(start + pageBytes, bytes.byteLength) - 1;
3059
+ let page: RetainedArtifactContent | null = null;
3060
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
3061
+ try {
3062
+ page = await this.getSessionRetainedArtifactContent(workspaceId, sessionId, artifactId, {
3063
+ range: `bytes=${start}-${end}`,
3064
+ ...(options.signal ? { signal: options.signal } : {}),
3065
+ });
3066
+ break;
3067
+ } catch (error) {
3068
+ options.signal?.throwIfAborted();
3069
+ if (
3070
+ attempt >= maxRetries ||
3071
+ (error instanceof OpenGeniApiError && error.status >= 400 && error.status < 500)
3072
+ ) {
3073
+ throw error;
3074
+ }
3075
+ }
3076
+ }
3077
+ if (!page) throw new OpenGeniApiError(502, "retained screenshot range retry exhausted");
3078
+ const expectedLength = end - start + 1;
3079
+ if (
3080
+ page.status !== 206 ||
3081
+ page.contentType !== metadata.contentType ||
3082
+ page.contentLength !== expectedLength ||
3083
+ page.contentRange !== `bytes ${start}-${end}/${metadata.originalBytes}`
3084
+ ) {
3085
+ throw new OpenGeniApiError(502, "retained screenshot range response is invalid");
3086
+ }
3087
+ bytes.set(page.bytes, start);
3088
+ }
3089
+ if ((await sha256Hex(bytes)) !== metadata.sha256) {
3090
+ throw new OpenGeniApiError(502, "retained screenshot checksum mismatch");
3091
+ }
3092
+ return { metadata, bytes };
3093
+ }
3094
+
3095
+ private async getRetainedArtifactContentAtPath(
3096
+ path: string,
3097
+ options: RetainedArtifactContentOptions,
2822
3098
  ): Promise<RetainedArtifactContent> {
2823
3099
  if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
2824
3100
  throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
2825
3101
  }
2826
3102
  const correlationId = crypto.randomUUID();
2827
- const response = await this.fetchImpl(
2828
- this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
2829
- {
2830
- method: "GET",
2831
- headers: {
2832
- ...this.headers(correlationId),
2833
- Accept: "application/octet-stream",
2834
- ...(options.range ? { Range: options.range } : {}),
2835
- },
2836
- ...(options.signal ? { signal: options.signal } : {}),
3103
+ const response = await this.fetchImpl(this.url(path), {
3104
+ method: "GET",
3105
+ headers: {
3106
+ ...this.headers(correlationId),
3107
+ Accept: "application/octet-stream",
3108
+ ...(options.range ? { Range: options.range } : {}),
2837
3109
  },
2838
- );
3110
+ ...(options.signal ? { signal: options.signal } : {}),
3111
+ });
2839
3112
  try {
2840
3113
  assertApiContractResponse(response);
2841
3114
  } catch (error) {
@@ -3772,7 +4045,12 @@ function isUploadTranscriptionRecordingChunkResponse(
3772
4045
  ): value is UploadTranscriptionRecordingChunkResponse {
3773
4046
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3774
4047
  const record = value as Record<string, unknown>;
3775
- if (!isTranscriptionRecordingResponse({ recording: record.recording, segments: [] })) {
4048
+ if (
4049
+ !isTranscriptionRecordingResponse({
4050
+ recording: record.recording,
4051
+ segments: [],
4052
+ })
4053
+ ) {
3776
4054
  return false;
3777
4055
  }
3778
4056
  if (!record.chunk || typeof record.chunk !== "object" || Array.isArray(record.chunk)) {
@@ -3947,6 +4225,12 @@ async function sha256ForUpload(body: Blob | ArrayBuffer | string): Promise<strin
3947
4225
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3948
4226
  }
3949
4227
 
4228
+ async function sha256Hex(bytes: Uint8Array): Promise<string> {
4229
+ const owned = Uint8Array.from(bytes);
4230
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", owned.buffer);
4231
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4232
+ }
4233
+
3950
4234
  async function cancelResponseBody(response: Response, reason: string): Promise<void> {
3951
4235
  await response.body?.cancel(reason).catch(() => undefined);
3952
4236
  }
package/src/index.ts CHANGED
@@ -578,6 +578,8 @@ export type {
578
578
  FsEncoding,
579
579
  FsListRequest,
580
580
  FsListResponse,
581
+ FsListBatchRequest,
582
+ FsListBatchResponse,
581
583
  FsReadRequest,
582
584
  FsReadResponse,
583
585
  FsWriteRequest,
@@ -598,6 +600,10 @@ export type {
598
600
  GitFileDiff,
599
601
  GitDiffRequest,
600
602
  GitDiffResponse,
603
+ GitReadBatchItemRequest,
604
+ GitReadBatchRequest,
605
+ GitReadBatchItemResponse,
606
+ GitReadBatchResponse,
601
607
  GitLogRequest,
602
608
  GitCommit,
603
609
  GitLogResponse,
@@ -646,6 +652,7 @@ export type {
646
652
  Workspace,
647
653
  WorkspaceControlEvent,
648
654
  VariableSet,
655
+ VariableSetSecret,
649
656
  VariableSetVariableMetadata,
650
657
  Rig,
651
658
  RigVersion,
@@ -677,6 +684,8 @@ export type {
677
684
  MachineView,
678
685
  MachinesResponse,
679
686
  MachineMetricsSeriesResponse,
687
+ RemoveEnrollmentRequest,
688
+ RemoveEnrollmentResponse,
680
689
  // Bring-your-own-compute: the user-authenticated active-sandbox swap (M7).
681
690
  SwapActiveSandboxRequest,
682
691
  SwapActiveSandboxResponse,