@opengeni/sdk 0.25.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/sdk",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Framework-agnostic TypeScript SDK for the OpenGeni API: typed client, session lifecycle, SSE event streaming with reconnect + replay-by-sequence, and proxy re-streaming helpers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/client.ts CHANGED
@@ -33,6 +33,8 @@ import type {
33
33
  CapabilityCatalogResponse,
34
34
  CapabilityInstallation,
35
35
  AddDocumentRequest,
36
+ CreateKnowledgeDropRequest,
37
+ MoveDocumentRequest,
36
38
  ClientConfig,
37
39
  WorkspaceModelCatalogResponse,
38
40
  ClientSessionEventInput,
@@ -44,6 +46,7 @@ import type {
44
46
  CreateCapabilityCatalogItemRequest,
45
47
  CreateCheckoutRequest,
46
48
  CreateCheckoutResponse,
49
+ ConnectOpenGeniSlackBotRequest,
47
50
  CreateConnectionRequest,
48
51
  CreateDocumentBaseRequest,
49
52
  CreateFileUploadRequest,
@@ -170,6 +173,7 @@ import type {
170
173
  UpdateScheduledTaskRequest,
171
174
  UpdateSessionGoalRequest,
172
175
  UpdateSessionRequest,
176
+ UpdateSessionToolPolicyRequest,
173
177
  UpdateVariableSetRequest,
174
178
  UpdateRigRequest,
175
179
  UpdateWorkspaceMemberRequest,
@@ -193,9 +197,22 @@ import type {
193
197
  OAuthStartRequest,
194
198
  OAuthStartResponse,
195
199
  } from "./types";
200
+ import type {
201
+ ActivateWorkspaceInstructionPolicyRequest,
202
+ CreateWorkspaceInstructionPolicyDraftRequest,
203
+ ImportLegacyWorkspaceInstructionPolicyDraftRequest,
204
+ RollbackWorkspaceInstructionPolicyRequest,
205
+ WorkspaceInstructionPolicyActivationResponse,
206
+ WorkspaceInstructionPolicyDiffRequest,
207
+ WorkspaceInstructionPolicyDiffResponse,
208
+ WorkspaceInstructionPolicyListOptions,
209
+ WorkspaceInstructionPolicyListResponse,
210
+ WorkspaceInstructionPolicyRevision,
211
+ } from "./workspace-instruction-policies";
196
212
  import {
197
213
  OPENGENI_API_CONTRACT_HEADER,
198
214
  OPENGENI_API_CONTRACT_REVISION,
215
+ OPENGENI_CORRELATION_HEADER,
199
216
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
200
217
  } from "./types";
201
218
 
@@ -323,6 +340,19 @@ export class OpenGeniClient {
323
340
  );
324
341
  }
325
342
 
343
+ /** Replace the durable tool policy or explicitly adopt workspace defaults. */
344
+ async updateSessionToolPolicy(
345
+ workspaceId: string,
346
+ sessionId: string,
347
+ request: UpdateSessionToolPolicyRequest,
348
+ ): Promise<Session> {
349
+ return await this.requestJson<Session>(
350
+ "PUT",
351
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/tool-policy`,
352
+ request,
353
+ );
354
+ }
355
+
326
356
  /**
327
357
  * Replace one attached MCP server's approval policy. The change is captured
328
358
  * by the next claimed attempt; already-claimed work keeps its immutable
@@ -396,7 +426,13 @@ export class OpenGeniClient {
396
426
  );
397
427
  } catch (error) {
398
428
  if (error instanceof OpenGeniApiError && error.status === 410) {
399
- throw new OpenGeniSessionListCursorError(error.status, error.body);
429
+ throw new OpenGeniSessionListCursorError(error.status, error.body, {
430
+ ...(error.code ? { code: error.code } : {}),
431
+ retryable: error.retryable,
432
+ ...(error.correlationId ? { correlationId: error.correlationId } : {}),
433
+ outcomeUnknown: error.outcomeUnknown,
434
+ displayMessage: "The session list changed — refresh and try again.",
435
+ });
400
436
  }
401
437
  throw error;
402
438
  }
@@ -650,6 +686,7 @@ export class OpenGeniClient {
650
686
  }
651
687
  const listOptions: SessionEventListOptions | null =
652
688
  options.resultMode === "compact" ? null : options;
689
+ const correlationId = crypto.randomUUID();
653
690
  const response = await this.fetchImpl(
654
691
  this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, {
655
692
  ...(listOptions?.after !== undefined ? { after: String(listOptions.after) } : {}),
@@ -676,11 +713,14 @@ export class OpenGeniClient {
676
713
  }),
677
714
  {
678
715
  method: "GET",
679
- headers: { ...this.headers(), Accept: "application/json" },
716
+ headers: { ...this.headers(correlationId), Accept: "application/json" },
680
717
  },
681
718
  );
682
719
  assertApiContractResponse(response);
683
- if (!response.ok) throw new OpenGeniApiError(response.status, await safeText(response));
720
+ if (!response.ok) {
721
+ throw await apiErrorFromResponse(response, { method: "GET", correlationId });
722
+ }
723
+ await assertJsonResponse(response, { method: "GET", correlationId });
684
724
  const body = await response.json();
685
725
  if (options.resultMode === "compact") {
686
726
  return body as SessionEventCompactResult;
@@ -880,14 +920,15 @@ export class OpenGeniClient {
880
920
  const url = this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events/stream`, {
881
921
  after: String(options.after ?? 0),
882
922
  });
923
+ const correlationId = crypto.randomUUID();
883
924
  const response = await this.fetchImpl(url, {
884
925
  method: "GET",
885
- headers: { ...this.headers(), Accept: "text/event-stream" },
926
+ headers: { ...this.headers(correlationId), Accept: "text/event-stream" },
886
927
  ...(options.signal ? { signal: options.signal } : {}),
887
928
  });
888
929
  assertApiContractResponse(response);
889
930
  if (!response.ok) {
890
- throw new OpenGeniApiError(response.status, await safeText(response));
931
+ throw await apiErrorFromResponse(response, { method: "GET", correlationId });
891
932
  }
892
933
  if (!response.body) {
893
934
  throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
@@ -1033,6 +1074,7 @@ export class OpenGeniClient {
1033
1074
  workspaceId: string,
1034
1075
  options: { after?: number; limit?: number } = {},
1035
1076
  ): Promise<WorkspaceControlEventPage> {
1077
+ const correlationId = crypto.randomUUID();
1036
1078
  const response = await this.fetchImpl(
1037
1079
  this.url(`/v1/workspaces/${workspaceId}/control-events`, {
1038
1080
  ...(options.after !== undefined ? { after: String(options.after) } : {}),
@@ -1040,13 +1082,14 @@ export class OpenGeniClient {
1040
1082
  }),
1041
1083
  {
1042
1084
  method: "GET",
1043
- headers: { ...this.headers(), Accept: "application/json" },
1085
+ headers: { ...this.headers(correlationId), Accept: "application/json" },
1044
1086
  },
1045
1087
  );
1046
1088
  assertApiContractResponse(response);
1047
1089
  if (!response.ok) {
1048
- throw new OpenGeniApiError(response.status, await safeText(response));
1090
+ throw await apiErrorFromResponse(response, { method: "GET", correlationId });
1049
1091
  }
1092
+ await assertJsonResponse(response, { method: "GET", correlationId });
1050
1093
  const events = (await response.json()) as WorkspaceControlEvent[];
1051
1094
  const bytesHeader = response.headers.get("X-OpenGeni-Page-Bytes");
1052
1095
  const nextHeader = response.headers.get("X-OpenGeni-Next-After");
@@ -1087,18 +1130,21 @@ export class OpenGeniClient {
1087
1130
  workspaceId: string,
1088
1131
  options: { after?: number; signal?: AbortSignal } = {},
1089
1132
  ): Promise<ReadableStream<Uint8Array>> {
1133
+ const correlationId = crypto.randomUUID();
1090
1134
  const response = await this.fetchImpl(
1091
1135
  this.url(`/v1/workspaces/${workspaceId}/control-events/stream`, {
1092
1136
  after: String(options.after ?? 0),
1093
1137
  }),
1094
1138
  {
1095
1139
  method: "GET",
1096
- headers: { ...this.headers(), Accept: "text/event-stream" },
1140
+ headers: { ...this.headers(correlationId), Accept: "text/event-stream" },
1097
1141
  ...(options.signal ? { signal: options.signal } : {}),
1098
1142
  },
1099
1143
  );
1100
1144
  assertApiContractResponse(response);
1101
- if (!response.ok) throw new OpenGeniApiError(response.status, await safeText(response));
1145
+ if (!response.ok) {
1146
+ throw await apiErrorFromResponse(response, { method: "GET", correlationId });
1147
+ }
1102
1148
  if (!response.body) {
1103
1149
  throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
1104
1150
  }
@@ -1580,6 +1626,96 @@ export class OpenGeniClient {
1580
1626
  return await this.requestJson<Workspace>("PATCH", `/v1/workspaces/${workspaceId}`, request);
1581
1627
  }
1582
1628
 
1629
+ /** Inspect immutable instruction-policy history, active heads, and activation audit evidence. */
1630
+ async listWorkspaceInstructionPolicies(
1631
+ workspaceId: string,
1632
+ options: WorkspaceInstructionPolicyListOptions = {},
1633
+ ): Promise<WorkspaceInstructionPolicyListResponse> {
1634
+ const params = new URLSearchParams();
1635
+ if (options.kind !== undefined) params.set("kind", options.kind);
1636
+ if (options.scope !== undefined) params.set("scope", options.scope);
1637
+ if (options.roleKey !== undefined) params.set("roleKey", options.roleKey);
1638
+ if (options.afterRevision !== undefined) {
1639
+ params.set("afterRevision", String(options.afterRevision));
1640
+ }
1641
+ if (options.limit !== undefined) params.set("limit", String(options.limit));
1642
+ const query = params.toString();
1643
+ return await this.requestJson<WorkspaceInstructionPolicyListResponse>(
1644
+ "GET",
1645
+ `/v1/workspaces/${workspaceId}/instruction-policies${query ? `?${query}` : ""}`,
1646
+ );
1647
+ }
1648
+
1649
+ async getWorkspaceInstructionPolicyRevision(
1650
+ workspaceId: string,
1651
+ revisionId: string,
1652
+ ): Promise<WorkspaceInstructionPolicyRevision> {
1653
+ return await this.requestJson<WorkspaceInstructionPolicyRevision>(
1654
+ "GET",
1655
+ `/v1/workspaces/${workspaceId}/instruction-policies/${encodeURIComponent(revisionId)}`,
1656
+ );
1657
+ }
1658
+
1659
+ async createWorkspaceInstructionPolicyDraft(
1660
+ workspaceId: string,
1661
+ request: CreateWorkspaceInstructionPolicyDraftRequest,
1662
+ ): Promise<WorkspaceInstructionPolicyRevision> {
1663
+ return await this.requestJson<WorkspaceInstructionPolicyRevision>(
1664
+ "POST",
1665
+ `/v1/workspaces/${workspaceId}/instruction-policies/drafts`,
1666
+ request,
1667
+ );
1668
+ }
1669
+
1670
+ /** Import the stored legacy workspace override as an inactive charter draft. */
1671
+ async importLegacyWorkspaceInstructionPolicyDraft(
1672
+ workspaceId: string,
1673
+ request: ImportLegacyWorkspaceInstructionPolicyDraftRequest = {},
1674
+ ): Promise<WorkspaceInstructionPolicyRevision> {
1675
+ return await this.requestJson<WorkspaceInstructionPolicyRevision>(
1676
+ "POST",
1677
+ `/v1/workspaces/${workspaceId}/instruction-policies/import-legacy`,
1678
+ request,
1679
+ );
1680
+ }
1681
+
1682
+ async diffWorkspaceInstructionPolicyRevisions(
1683
+ workspaceId: string,
1684
+ request: WorkspaceInstructionPolicyDiffRequest,
1685
+ ): Promise<WorkspaceInstructionPolicyDiffResponse> {
1686
+ const params = new URLSearchParams({
1687
+ fromRevisionId: request.fromRevisionId,
1688
+ toRevisionId: request.toRevisionId,
1689
+ });
1690
+ return await this.requestJson<WorkspaceInstructionPolicyDiffResponse>(
1691
+ "GET",
1692
+ `/v1/workspaces/${workspaceId}/instruction-policies/diff?${params}`,
1693
+ );
1694
+ }
1695
+
1696
+ async activateWorkspaceInstructionPolicyRevision(
1697
+ workspaceId: string,
1698
+ revisionId: string,
1699
+ request: ActivateWorkspaceInstructionPolicyRequest,
1700
+ ): Promise<WorkspaceInstructionPolicyActivationResponse> {
1701
+ return await this.requestJson<WorkspaceInstructionPolicyActivationResponse>(
1702
+ "POST",
1703
+ `/v1/workspaces/${workspaceId}/instruction-policies/${encodeURIComponent(revisionId)}/activate`,
1704
+ request,
1705
+ );
1706
+ }
1707
+
1708
+ async rollbackWorkspaceInstructionPolicyRevision(
1709
+ workspaceId: string,
1710
+ request: RollbackWorkspaceInstructionPolicyRequest,
1711
+ ): Promise<WorkspaceInstructionPolicyActivationResponse> {
1712
+ return await this.requestJson<WorkspaceInstructionPolicyActivationResponse>(
1713
+ "POST",
1714
+ `/v1/workspaces/${workspaceId}/instruction-policies/rollback`,
1715
+ request,
1716
+ );
1717
+ }
1718
+
1583
1719
  /**
1584
1720
  * Delete a workspace and everything in it. Refused (409) for the account's
1585
1721
  * only workspace and while it still has a running session. Irreversible.
@@ -2013,7 +2149,7 @@ export class OpenGeniClient {
2013
2149
  body,
2014
2150
  });
2015
2151
  if (!putResponse.ok) {
2016
- throw new OpenGeniApiError(putResponse.status, await safeText(putResponse));
2152
+ throw await apiErrorFromResponse(putResponse, { method: "PUT" });
2017
2153
  }
2018
2154
  return await this.completeFileUpload(workspaceId, upload.uploadId);
2019
2155
  }
@@ -2048,12 +2184,13 @@ export class OpenGeniClient {
2048
2184
  if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
2049
2185
  throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
2050
2186
  }
2187
+ const correlationId = crypto.randomUUID();
2051
2188
  const response = await this.fetchImpl(
2052
2189
  this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
2053
2190
  {
2054
2191
  method: "GET",
2055
2192
  headers: {
2056
- ...this.headers(),
2193
+ ...this.headers(correlationId),
2057
2194
  Accept: "application/octet-stream",
2058
2195
  ...(options.range ? { Range: options.range } : {}),
2059
2196
  },
@@ -2067,7 +2204,7 @@ export class OpenGeniClient {
2067
2204
  throw error;
2068
2205
  }
2069
2206
  if (!response.ok) {
2070
- throw new OpenGeniApiError(response.status, await safeBoundedText(response));
2207
+ throw await apiErrorFromResponse(response, { method: "GET", correlationId });
2071
2208
  }
2072
2209
  if (response.status !== 200 && response.status !== 206) {
2073
2210
  await cancelResponseBody(response, "unexpected retained artifact response status");
@@ -2157,6 +2294,39 @@ export class OpenGeniClient {
2157
2294
  );
2158
2295
  }
2159
2296
 
2297
+ /**
2298
+ * Drop raw text or an already-uploaded file into the workspace's Default
2299
+ * base. When curation is enabled, it may name, summarize, categorize, and
2300
+ * (confidence permitting) file the document into the best-matching base;
2301
+ * provider=none leaves caller metadata and Default placement unchanged.
2302
+ */
2303
+ async createKnowledgeDrop(
2304
+ workspaceId: string,
2305
+ request: CreateKnowledgeDropRequest,
2306
+ ): Promise<Document> {
2307
+ return await this.requestJson<Document>(
2308
+ "POST",
2309
+ `/v1/workspaces/${workspaceId}/knowledge/drops`,
2310
+ request,
2311
+ );
2312
+ }
2313
+
2314
+ /**
2315
+ * Move a document (and its indexed chunks) to another base. With no
2316
+ * targetBaseId, applies the document's stored curation suggestion.
2317
+ */
2318
+ async moveDocument(
2319
+ workspaceId: string,
2320
+ documentId: string,
2321
+ request: MoveDocumentRequest = {},
2322
+ ): Promise<Document> {
2323
+ return await this.requestJson<Document>(
2324
+ "POST",
2325
+ `/v1/workspaces/${workspaceId}/documents/${documentId}/move`,
2326
+ request,
2327
+ );
2328
+ }
2329
+
2160
2330
  /** Retry indexing for a failed document. */
2161
2331
  async reindexDocument(
2162
2332
  workspaceId: string,
@@ -2419,6 +2589,19 @@ export class OpenGeniClient {
2419
2589
  return response.connection;
2420
2590
  }
2421
2591
 
2592
+ /** Validate and store/reinstall the workspace-shared OpenGeni Slack bot credential. */
2593
+ async connectOpenGeniSlackBot(
2594
+ workspaceId: string,
2595
+ request: ConnectOpenGeniSlackBotRequest,
2596
+ ): Promise<ConnectionMetadata> {
2597
+ const response = await this.requestJson<ConnectionResponse>(
2598
+ "POST",
2599
+ `/v1/workspaces/${workspaceId}/connections/slack-bot`,
2600
+ request,
2601
+ );
2602
+ return response.connection;
2603
+ }
2604
+
2422
2605
  async updateConnection(
2423
2606
  workspaceId: string,
2424
2607
  connectionId: string,
@@ -2459,15 +2642,12 @@ export class OpenGeniClient {
2459
2642
 
2460
2643
  // --- GitHub ----------------------------------------------------------------------------------
2461
2644
 
2462
- /** GitHub App configuration status; install/link URLs are null while new binding is disabled. */
2645
+ /** GitHub App server configuration plus truthful workspace binding status. */
2463
2646
  async getGitHubApp(workspaceId: string): Promise<GitHubAppInfo> {
2464
2647
  return await this.requestJson<GitHubAppInfo>("GET", `/v1/workspaces/${workspaceId}/github/app`);
2465
2648
  }
2466
2649
 
2467
- /**
2468
- * Compatibility URL for previously issued state. New installation binding is
2469
- * disabled, so the endpoint validates state and terminates with HTTP 410.
2470
- */
2650
+ /** Build the GitHub owner-consent entry URL for fresh workspace-bound state. */
2471
2651
  githubConnectUrl(workspaceId: string, state: string): string {
2472
2652
  return this.url(`/v1/workspaces/${workspaceId}/github/connect`, { state });
2473
2653
  }
@@ -2574,13 +2754,14 @@ export class OpenGeniClient {
2574
2754
 
2575
2755
  // --- Internals -------------------------------------------------------------
2576
2756
 
2577
- private headers(): Record<string, string> {
2757
+ private headers(correlationId?: string): Record<string, string> {
2578
2758
  const extra =
2579
2759
  typeof this.options.headers === "function" ? this.options.headers() : this.options.headers;
2580
2760
  return {
2581
2761
  ...(this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}),
2582
2762
  ...extra,
2583
2763
  [OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
2764
+ ...(correlationId ? { [OPENGENI_CORRELATION_HEADER]: correlationId } : {}),
2584
2765
  };
2585
2766
  }
2586
2767
 
@@ -2744,37 +2925,63 @@ export class OpenGeniClient {
2744
2925
  query: Record<string, string> = {},
2745
2926
  options: OpenGeniRequestOptions = {},
2746
2927
  ): Promise<T> {
2747
- const response = await this.fetchImpl(this.url(path, query), {
2748
- method,
2749
- headers: {
2750
- ...this.headers(),
2751
- Accept: "application/json",
2752
- ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
2753
- },
2754
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2755
- ...(options.signal ? { signal: options.signal } : {}),
2756
- });
2928
+ const correlationId = crypto.randomUUID();
2929
+ let response: Response;
2930
+ try {
2931
+ response = await this.fetchImpl(this.url(path, query), {
2932
+ method,
2933
+ headers: {
2934
+ ...this.headers(correlationId),
2935
+ Accept: "application/json",
2936
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
2937
+ },
2938
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2939
+ ...(options.signal ? { signal: options.signal } : {}),
2940
+ });
2941
+ } catch (error) {
2942
+ if (isMutationMethod(method)) {
2943
+ throw mutationTransportError(correlationId);
2944
+ }
2945
+ throw error;
2946
+ }
2757
2947
  assertApiContractResponse(response);
2758
2948
  if (!response.ok) {
2759
- throw new OpenGeniApiError(response.status, await safeText(response));
2949
+ throw await apiErrorFromResponse(response, { method, correlationId });
2950
+ }
2951
+ await assertJsonResponse(response, { method, correlationId });
2952
+ try {
2953
+ return (await response.json()) as T;
2954
+ } catch (error) {
2955
+ if (isMutationMethod(method)) {
2956
+ throw mutationTransportError(correlationId);
2957
+ }
2958
+ throw error;
2760
2959
  }
2761
- return (await response.json()) as T;
2762
2960
  }
2763
2961
 
2764
2962
  /** Like `requestJson` for endpoints that respond with no body (204). */
2765
2963
  private async requestVoid(method: string, path: string, body?: unknown): Promise<void> {
2766
- const response = await this.fetchImpl(this.url(path), {
2767
- method,
2768
- headers: {
2769
- ...this.headers(),
2770
- Accept: "application/json",
2771
- ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
2772
- },
2773
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2774
- });
2964
+ const correlationId = crypto.randomUUID();
2965
+ let response: Response;
2966
+ try {
2967
+ response = await this.fetchImpl(this.url(path), {
2968
+ method,
2969
+ headers: {
2970
+ ...this.headers(correlationId),
2971
+ Accept: "application/json",
2972
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
2973
+ },
2974
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2975
+ });
2976
+ } catch (error) {
2977
+ if (isMutationMethod(method)) {
2978
+ throw mutationTransportError(correlationId);
2979
+ }
2980
+ throw error;
2981
+ }
2775
2982
  assertApiContractResponse(response);
2776
2983
  if (!response.ok) {
2777
- throw new OpenGeniApiError(response.status, await safeText(response));
2984
+ throw await apiErrorFromResponse(response, { method, correlationId });
2778
2985
  }
2779
2986
  }
2780
2987
  }
@@ -2786,6 +2993,75 @@ function assertApiContractResponse(response: Response): void {
2786
2993
  }
2787
2994
  }
2788
2995
 
2996
+ const API_ERROR_MAX_BYTES = 16 * 1024;
2997
+
2998
+ type ApiErrorRequestContext = {
2999
+ method: string;
3000
+ correlationId?: string | undefined;
3001
+ };
3002
+
3003
+ async function apiErrorFromResponse(
3004
+ response: Response,
3005
+ context: ApiErrorRequestContext,
3006
+ ): Promise<OpenGeniApiError> {
3007
+ return new OpenGeniApiError(response.status, await readBoundedJsonErrorBody(response), {
3008
+ correlationId: response.headers.get(OPENGENI_CORRELATION_HEADER) ?? context.correlationId,
3009
+ mutation: isMutationMethod(context.method),
3010
+ });
3011
+ }
3012
+
3013
+ async function assertJsonResponse(
3014
+ response: Response,
3015
+ context: ApiErrorRequestContext,
3016
+ ): Promise<void> {
3017
+ if (isJsonContentType(response.headers.get("content-type"))) return;
3018
+ await cancelResponseBody(response, "unexpected non-JSON API response");
3019
+ throw new OpenGeniApiError(502, "", {
3020
+ code: "upstream_unavailable",
3021
+ retryable: true,
3022
+ correlationId: response.headers.get(OPENGENI_CORRELATION_HEADER) ?? context.correlationId,
3023
+ outcomeUnknown: isMutationMethod(context.method),
3024
+ displayMessage: "OpenGeni is temporarily unavailable — retry.",
3025
+ });
3026
+ }
3027
+
3028
+ async function readBoundedJsonErrorBody(response: Response): Promise<string> {
3029
+ if (!isJsonContentType(response.headers.get("content-type"))) {
3030
+ await cancelResponseBody(response, "discarding API error body");
3031
+ return "";
3032
+ }
3033
+ if (Number(response.headers.get("content-length")) > API_ERROR_MAX_BYTES) {
3034
+ await cancelResponseBody(response, "discarding API error body");
3035
+ return "";
3036
+ }
3037
+ try {
3038
+ return new TextDecoder().decode(
3039
+ await readBoundedResponseBytes(response, API_ERROR_MAX_BYTES, null),
3040
+ );
3041
+ } catch {
3042
+ return "";
3043
+ }
3044
+ }
3045
+
3046
+ function isJsonContentType(value: string | null): boolean {
3047
+ return /^(application\/json|[^;]+\+json)\s*(;|$)/i.test(value ?? "");
3048
+ }
3049
+
3050
+ function isMutationMethod(method: string): boolean {
3051
+ return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
3052
+ }
3053
+
3054
+ function mutationTransportError(correlationId: string): OpenGeniApiError {
3055
+ return new OpenGeniApiError(0, "", {
3056
+ code: "network_error",
3057
+ retryable: true,
3058
+ correlationId,
3059
+ outcomeUnknown: true,
3060
+ mutation: true,
3061
+ displayMessage: "OpenGeni could not confirm the request — reconcile before retrying.",
3062
+ });
3063
+ }
3064
+
2789
3065
  async function sha256ForUpload(body: Blob | ArrayBuffer | string): Promise<string> {
2790
3066
  const bytes =
2791
3067
  typeof body === "string"
@@ -2797,22 +3073,6 @@ async function sha256ForUpload(body: Blob | ArrayBuffer | string): Promise<strin
2797
3073
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2798
3074
  }
2799
3075
 
2800
- async function safeText(response: Response): Promise<string> {
2801
- try {
2802
- return await response.text();
2803
- } catch {
2804
- return "";
2805
- }
2806
- }
2807
-
2808
- async function safeBoundedText(response: Response): Promise<string> {
2809
- try {
2810
- return new TextDecoder().decode(await readBoundedResponseBytes(response, 64 * 1024, null));
2811
- } catch {
2812
- return "";
2813
- }
2814
- }
2815
-
2816
3076
  async function cancelResponseBody(response: Response, reason: string): Promise<void> {
2817
3077
  await response.body?.cancel(reason).catch(() => undefined);
2818
3078
  }