@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/README.md +52 -0
- package/dist/index.d.ts +202 -10
- package/dist/index.js +285 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +316 -56
- package/src/errors.ts +75 -19
- package/src/index.ts +33 -0
- package/src/types.ts +74 -2
- package/src/workspace-instruction-policies.ts +124 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/sdk",
|
|
3
|
-
"version": "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)
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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
|
|
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
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
|
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
|
}
|