@opengeni/sdk 0.23.0 → 0.25.5
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 +206 -14
- package/dist/index.js +286 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +302 -81
- package/src/errors.ts +89 -12
- package/src/index.ts +18 -0
- package/src/types.ts +236 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.5",
|
|
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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
OpenGeniApiContractMismatchError,
|
|
3
|
+
OpenGeniApiError,
|
|
4
|
+
OpenGeniSessionListCursorError,
|
|
5
|
+
} from "./errors";
|
|
2
6
|
import {
|
|
3
7
|
streamSessionEvents,
|
|
4
8
|
type SessionEventStreamTransport,
|
|
@@ -29,6 +33,8 @@ import type {
|
|
|
29
33
|
CapabilityCatalogResponse,
|
|
30
34
|
CapabilityInstallation,
|
|
31
35
|
AddDocumentRequest,
|
|
36
|
+
CreateKnowledgeDropRequest,
|
|
37
|
+
MoveDocumentRequest,
|
|
32
38
|
ClientConfig,
|
|
33
39
|
WorkspaceModelCatalogResponse,
|
|
34
40
|
ClientSessionEventInput,
|
|
@@ -112,7 +118,9 @@ import type {
|
|
|
112
118
|
DeleteSessionQueueItemRequest,
|
|
113
119
|
EditSessionQueueItemRequest,
|
|
114
120
|
MoveSessionQueueItemRequest,
|
|
121
|
+
NewSessionDraft,
|
|
115
122
|
SaveComposerDraftRequest,
|
|
123
|
+
SaveNewSessionDraftRequest,
|
|
116
124
|
SteerSessionQueueItemRequest,
|
|
117
125
|
SessionControlResponse,
|
|
118
126
|
WorkspaceInferenceControlResponse,
|
|
@@ -164,6 +172,7 @@ import type {
|
|
|
164
172
|
UpdateScheduledTaskRequest,
|
|
165
173
|
UpdateSessionGoalRequest,
|
|
166
174
|
UpdateSessionRequest,
|
|
175
|
+
UpdateSessionToolPolicyRequest,
|
|
167
176
|
UpdateVariableSetRequest,
|
|
168
177
|
UpdateRigRequest,
|
|
169
178
|
UpdateWorkspaceMemberRequest,
|
|
@@ -190,9 +199,21 @@ import type {
|
|
|
190
199
|
import {
|
|
191
200
|
OPENGENI_API_CONTRACT_HEADER,
|
|
192
201
|
OPENGENI_API_CONTRACT_REVISION,
|
|
202
|
+
OPENGENI_CORRELATION_HEADER,
|
|
193
203
|
RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
194
204
|
} from "./types";
|
|
195
205
|
|
|
206
|
+
function sessionListQuery(options: {
|
|
207
|
+
limit?: number;
|
|
208
|
+
parentSessionId?: string | null;
|
|
209
|
+
}): Record<string, string> {
|
|
210
|
+
const { limit, parentSessionId } = options;
|
|
211
|
+
return {
|
|
212
|
+
...(limit === undefined ? {} : { limit: String(limit) }),
|
|
213
|
+
...(parentSessionId === undefined ? {} : { parentSessionId: parentSessionId ?? "null" }),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
197
218
|
|
|
198
219
|
export type WorkspaceControlEventPage = {
|
|
@@ -269,6 +290,24 @@ export class OpenGeniClient {
|
|
|
269
290
|
);
|
|
270
291
|
}
|
|
271
292
|
|
|
293
|
+
async getNewSessionDraft(workspaceId: string): Promise<NewSessionDraft> {
|
|
294
|
+
return await this.requestJson<NewSessionDraft>(
|
|
295
|
+
"GET",
|
|
296
|
+
`/v1/workspaces/${workspaceId}/new-session-draft`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async saveNewSessionDraft(
|
|
301
|
+
workspaceId: string,
|
|
302
|
+
request: SaveNewSessionDraftRequest,
|
|
303
|
+
): Promise<NewSessionDraft> {
|
|
304
|
+
return await this.requestJson<NewSessionDraft>(
|
|
305
|
+
"PUT",
|
|
306
|
+
`/v1/workspaces/${workspaceId}/new-session-draft`,
|
|
307
|
+
request,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
272
311
|
async getSession(workspaceId: string, sessionId: string): Promise<Session> {
|
|
273
312
|
return await this.requestJson<Session>(
|
|
274
313
|
"GET",
|
|
@@ -288,6 +327,19 @@ export class OpenGeniClient {
|
|
|
288
327
|
);
|
|
289
328
|
}
|
|
290
329
|
|
|
330
|
+
/** Replace the durable tool policy or explicitly adopt workspace defaults. */
|
|
331
|
+
async updateSessionToolPolicy(
|
|
332
|
+
workspaceId: string,
|
|
333
|
+
sessionId: string,
|
|
334
|
+
request: UpdateSessionToolPolicyRequest,
|
|
335
|
+
): Promise<Session> {
|
|
336
|
+
return await this.requestJson<Session>(
|
|
337
|
+
"PUT",
|
|
338
|
+
`/v1/workspaces/${workspaceId}/sessions/${sessionId}/tool-policy`,
|
|
339
|
+
request,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
291
343
|
/**
|
|
292
344
|
* Replace one attached MCP server's approval policy. The change is captured
|
|
293
345
|
* by the next claimed attempt; already-claimed work keeps its immutable
|
|
@@ -314,21 +366,21 @@ export class OpenGeniClient {
|
|
|
314
366
|
search?: string;
|
|
315
367
|
} = {},
|
|
316
368
|
): Promise<Session[]> {
|
|
369
|
+
// Search was added with the pin-aware page endpoint. An older API silently
|
|
370
|
+
// ignores unknown query parameters on the historical array endpoint, which
|
|
371
|
+
// would turn a search into a plausible-looking unfiltered result. Route
|
|
372
|
+
// searches through listSessionPage so its rolling-version shape check can
|
|
373
|
+
// fail explicitly on an older server; retain the array endpoint for every
|
|
374
|
+
// pre-existing call shape.
|
|
375
|
+
if (options.search?.trim()) {
|
|
376
|
+
const page = await this.listSessionPage(workspaceId, options);
|
|
377
|
+
return [...page.pinned, ...page.sessions];
|
|
378
|
+
}
|
|
317
379
|
return await this.requestJson<Session[]>(
|
|
318
380
|
"GET",
|
|
319
381
|
`/v1/workspaces/${workspaceId}/sessions`,
|
|
320
382
|
undefined,
|
|
321
|
-
|
|
322
|
-
...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
|
|
323
|
-
...(options.search?.trim() ? { search: options.search.trim() } : {}),
|
|
324
|
-
...(Object.prototype.hasOwnProperty.call(options, "parentSessionId") &&
|
|
325
|
-
options.parentSessionId !== undefined
|
|
326
|
-
? {
|
|
327
|
-
parentSessionId:
|
|
328
|
-
options.parentSessionId === null ? "null" : String(options.parentSessionId),
|
|
329
|
-
}
|
|
330
|
-
: {}),
|
|
331
|
-
},
|
|
383
|
+
sessionListQuery(options),
|
|
332
384
|
);
|
|
333
385
|
}
|
|
334
386
|
|
|
@@ -340,26 +392,57 @@ export class OpenGeniClient {
|
|
|
340
392
|
parentSessionId?: string | null;
|
|
341
393
|
cursor?: string;
|
|
342
394
|
search?: string;
|
|
395
|
+
/** Return only the complete personal pinned projection. */
|
|
396
|
+
pinsOnly?: boolean;
|
|
343
397
|
} = {},
|
|
344
398
|
): Promise<SessionListResponse> {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
? {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
399
|
+
const search = options.search?.trim();
|
|
400
|
+
let response: SessionListResponse | Session[];
|
|
401
|
+
try {
|
|
402
|
+
response = await this.requestJson<SessionListResponse | Session[]>(
|
|
403
|
+
"GET",
|
|
404
|
+
`/v1/workspaces/${workspaceId}/sessions`,
|
|
405
|
+
undefined,
|
|
406
|
+
{
|
|
407
|
+
view: "page",
|
|
408
|
+
...sessionListQuery(options),
|
|
409
|
+
...(options.cursor !== undefined ? { cursor: options.cursor } : {}),
|
|
410
|
+
...(search ? { search } : {}),
|
|
411
|
+
...(options.pinsOnly ? { pinsOnly: "true" } : {}),
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error instanceof OpenGeniApiError && error.status === 410) {
|
|
416
|
+
throw new OpenGeniSessionListCursorError(error.status, error.body, {
|
|
417
|
+
...(error.code ? { code: error.code } : {}),
|
|
418
|
+
retryable: error.retryable,
|
|
419
|
+
...(error.correlationId ? { correlationId: error.correlationId } : {}),
|
|
420
|
+
outcomeUnknown: error.outcomeUnknown,
|
|
421
|
+
displayMessage: "The session list changed — refresh and try again.",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
if (Array.isArray(response)) {
|
|
427
|
+
// Rolling/same-major compatibility: an older API ignores `view=page` and
|
|
428
|
+
// returns the historical array. That is an honest one-page projection;
|
|
429
|
+
// never pretend it honored a cursor supplied directly by a caller.
|
|
430
|
+
if (options.cursor) {
|
|
431
|
+
throw new Error("The connected OpenGeni API does not support stable session-page cursors");
|
|
432
|
+
}
|
|
433
|
+
// Older APIs ignore unknown query parameters. Treating their unfiltered
|
|
434
|
+
// array as a successful search would be worse than an explicit rolling-
|
|
435
|
+
// upgrade error (and client-side filtering cannot recover matches beyond
|
|
436
|
+
// the old endpoint's bounded first page).
|
|
437
|
+
if (search) {
|
|
438
|
+
throw new Error("The connected OpenGeni API does not support session search");
|
|
439
|
+
}
|
|
440
|
+
if (options.pinsOnly) {
|
|
441
|
+
throw new Error("The connected OpenGeni API does not support pins-only session lists");
|
|
442
|
+
}
|
|
443
|
+
return { pinned: [], sessions: response, nextCursor: null };
|
|
444
|
+
}
|
|
445
|
+
return response;
|
|
363
446
|
}
|
|
364
447
|
|
|
365
448
|
/** Set this authenticated member's personal workspace pin for a session. */
|
|
@@ -590,6 +673,7 @@ export class OpenGeniClient {
|
|
|
590
673
|
}
|
|
591
674
|
const listOptions: SessionEventListOptions | null =
|
|
592
675
|
options.resultMode === "compact" ? null : options;
|
|
676
|
+
const correlationId = crypto.randomUUID();
|
|
593
677
|
const response = await this.fetchImpl(
|
|
594
678
|
this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, {
|
|
595
679
|
...(listOptions?.after !== undefined ? { after: String(listOptions.after) } : {}),
|
|
@@ -616,11 +700,14 @@ export class OpenGeniClient {
|
|
|
616
700
|
}),
|
|
617
701
|
{
|
|
618
702
|
method: "GET",
|
|
619
|
-
headers: { ...this.headers(), Accept: "application/json" },
|
|
703
|
+
headers: { ...this.headers(correlationId), Accept: "application/json" },
|
|
620
704
|
},
|
|
621
705
|
);
|
|
622
706
|
assertApiContractResponse(response);
|
|
623
|
-
if (!response.ok)
|
|
707
|
+
if (!response.ok) {
|
|
708
|
+
throw await apiErrorFromResponse(response, { method: "GET", correlationId });
|
|
709
|
+
}
|
|
710
|
+
await assertJsonResponse(response, { method: "GET", correlationId });
|
|
624
711
|
const body = await response.json();
|
|
625
712
|
if (options.resultMode === "compact") {
|
|
626
713
|
return body as SessionEventCompactResult;
|
|
@@ -820,14 +907,15 @@ export class OpenGeniClient {
|
|
|
820
907
|
const url = this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events/stream`, {
|
|
821
908
|
after: String(options.after ?? 0),
|
|
822
909
|
});
|
|
910
|
+
const correlationId = crypto.randomUUID();
|
|
823
911
|
const response = await this.fetchImpl(url, {
|
|
824
912
|
method: "GET",
|
|
825
|
-
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
913
|
+
headers: { ...this.headers(correlationId), Accept: "text/event-stream" },
|
|
826
914
|
...(options.signal ? { signal: options.signal } : {}),
|
|
827
915
|
});
|
|
828
916
|
assertApiContractResponse(response);
|
|
829
917
|
if (!response.ok) {
|
|
830
|
-
throw
|
|
918
|
+
throw await apiErrorFromResponse(response, { method: "GET", correlationId });
|
|
831
919
|
}
|
|
832
920
|
if (!response.body) {
|
|
833
921
|
throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
|
|
@@ -973,6 +1061,7 @@ export class OpenGeniClient {
|
|
|
973
1061
|
workspaceId: string,
|
|
974
1062
|
options: { after?: number; limit?: number } = {},
|
|
975
1063
|
): Promise<WorkspaceControlEventPage> {
|
|
1064
|
+
const correlationId = crypto.randomUUID();
|
|
976
1065
|
const response = await this.fetchImpl(
|
|
977
1066
|
this.url(`/v1/workspaces/${workspaceId}/control-events`, {
|
|
978
1067
|
...(options.after !== undefined ? { after: String(options.after) } : {}),
|
|
@@ -980,13 +1069,14 @@ export class OpenGeniClient {
|
|
|
980
1069
|
}),
|
|
981
1070
|
{
|
|
982
1071
|
method: "GET",
|
|
983
|
-
headers: { ...this.headers(), Accept: "application/json" },
|
|
1072
|
+
headers: { ...this.headers(correlationId), Accept: "application/json" },
|
|
984
1073
|
},
|
|
985
1074
|
);
|
|
986
1075
|
assertApiContractResponse(response);
|
|
987
1076
|
if (!response.ok) {
|
|
988
|
-
throw
|
|
1077
|
+
throw await apiErrorFromResponse(response, { method: "GET", correlationId });
|
|
989
1078
|
}
|
|
1079
|
+
await assertJsonResponse(response, { method: "GET", correlationId });
|
|
990
1080
|
const events = (await response.json()) as WorkspaceControlEvent[];
|
|
991
1081
|
const bytesHeader = response.headers.get("X-OpenGeni-Page-Bytes");
|
|
992
1082
|
const nextHeader = response.headers.get("X-OpenGeni-Next-After");
|
|
@@ -1027,18 +1117,21 @@ export class OpenGeniClient {
|
|
|
1027
1117
|
workspaceId: string,
|
|
1028
1118
|
options: { after?: number; signal?: AbortSignal } = {},
|
|
1029
1119
|
): Promise<ReadableStream<Uint8Array>> {
|
|
1120
|
+
const correlationId = crypto.randomUUID();
|
|
1030
1121
|
const response = await this.fetchImpl(
|
|
1031
1122
|
this.url(`/v1/workspaces/${workspaceId}/control-events/stream`, {
|
|
1032
1123
|
after: String(options.after ?? 0),
|
|
1033
1124
|
}),
|
|
1034
1125
|
{
|
|
1035
1126
|
method: "GET",
|
|
1036
|
-
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
1127
|
+
headers: { ...this.headers(correlationId), Accept: "text/event-stream" },
|
|
1037
1128
|
...(options.signal ? { signal: options.signal } : {}),
|
|
1038
1129
|
},
|
|
1039
1130
|
);
|
|
1040
1131
|
assertApiContractResponse(response);
|
|
1041
|
-
if (!response.ok)
|
|
1132
|
+
if (!response.ok) {
|
|
1133
|
+
throw await apiErrorFromResponse(response, { method: "GET", correlationId });
|
|
1134
|
+
}
|
|
1042
1135
|
if (!response.body) {
|
|
1043
1136
|
throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
|
|
1044
1137
|
}
|
|
@@ -1919,21 +2012,27 @@ export class OpenGeniClient {
|
|
|
1919
2012
|
* -> complete. Returns the ready `FileAsset`.
|
|
1920
2013
|
*/
|
|
1921
2014
|
async uploadFile(workspaceId: string, input: UploadFileInput): Promise<FileAsset> {
|
|
1922
|
-
//
|
|
1923
|
-
//
|
|
2015
|
+
// Snapshot mutable inputs before hashing so the digest always describes the
|
|
2016
|
+
// exact bytes later sent to object storage. Copy Uint8Array views into a
|
|
2017
|
+
// Blob so byte offsets/shared buffers can't leak surrounding bytes.
|
|
1924
2018
|
const body: Blob | ArrayBuffer | string =
|
|
1925
|
-
input.data instanceof Uint8Array
|
|
2019
|
+
input.data instanceof Uint8Array
|
|
2020
|
+
? new Blob([input.data.slice()])
|
|
2021
|
+
: input.data instanceof ArrayBuffer
|
|
2022
|
+
? input.data.slice(0)
|
|
2023
|
+
: input.data;
|
|
1926
2024
|
const sizeBytes =
|
|
1927
2025
|
typeof body === "string"
|
|
1928
2026
|
? new TextEncoder().encode(body).byteLength
|
|
1929
2027
|
: body instanceof Blob
|
|
1930
2028
|
? body.size
|
|
1931
2029
|
: body.byteLength;
|
|
2030
|
+
const sha256 = input.sha256 ?? (await sha256ForUpload(body));
|
|
1932
2031
|
const upload = await this.beginFileUpload(workspaceId, {
|
|
1933
2032
|
filename: input.filename,
|
|
1934
2033
|
contentType: input.contentType,
|
|
1935
2034
|
sizeBytes,
|
|
1936
|
-
|
|
2035
|
+
sha256,
|
|
1937
2036
|
});
|
|
1938
2037
|
const putResponse = await this.fetchImpl(upload.putUrl, {
|
|
1939
2038
|
method: "PUT",
|
|
@@ -1947,7 +2046,7 @@ export class OpenGeniClient {
|
|
|
1947
2046
|
body,
|
|
1948
2047
|
});
|
|
1949
2048
|
if (!putResponse.ok) {
|
|
1950
|
-
throw
|
|
2049
|
+
throw await apiErrorFromResponse(putResponse, { method: "PUT" });
|
|
1951
2050
|
}
|
|
1952
2051
|
return await this.completeFileUpload(workspaceId, upload.uploadId);
|
|
1953
2052
|
}
|
|
@@ -1982,12 +2081,13 @@ export class OpenGeniClient {
|
|
|
1982
2081
|
if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
|
|
1983
2082
|
throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
|
|
1984
2083
|
}
|
|
2084
|
+
const correlationId = crypto.randomUUID();
|
|
1985
2085
|
const response = await this.fetchImpl(
|
|
1986
2086
|
this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
|
|
1987
2087
|
{
|
|
1988
2088
|
method: "GET",
|
|
1989
2089
|
headers: {
|
|
1990
|
-
...this.headers(),
|
|
2090
|
+
...this.headers(correlationId),
|
|
1991
2091
|
Accept: "application/octet-stream",
|
|
1992
2092
|
...(options.range ? { Range: options.range } : {}),
|
|
1993
2093
|
},
|
|
@@ -2001,7 +2101,7 @@ export class OpenGeniClient {
|
|
|
2001
2101
|
throw error;
|
|
2002
2102
|
}
|
|
2003
2103
|
if (!response.ok) {
|
|
2004
|
-
throw
|
|
2104
|
+
throw await apiErrorFromResponse(response, { method: "GET", correlationId });
|
|
2005
2105
|
}
|
|
2006
2106
|
if (response.status !== 200 && response.status !== 206) {
|
|
2007
2107
|
await cancelResponseBody(response, "unexpected retained artifact response status");
|
|
@@ -2091,6 +2191,39 @@ export class OpenGeniClient {
|
|
|
2091
2191
|
);
|
|
2092
2192
|
}
|
|
2093
2193
|
|
|
2194
|
+
/**
|
|
2195
|
+
* Drop raw text or an already-uploaded file into the workspace's Default
|
|
2196
|
+
* base. When curation is enabled, it may name, summarize, categorize, and
|
|
2197
|
+
* (confidence permitting) file the document into the best-matching base;
|
|
2198
|
+
* provider=none leaves caller metadata and Default placement unchanged.
|
|
2199
|
+
*/
|
|
2200
|
+
async createKnowledgeDrop(
|
|
2201
|
+
workspaceId: string,
|
|
2202
|
+
request: CreateKnowledgeDropRequest,
|
|
2203
|
+
): Promise<Document> {
|
|
2204
|
+
return await this.requestJson<Document>(
|
|
2205
|
+
"POST",
|
|
2206
|
+
`/v1/workspaces/${workspaceId}/knowledge/drops`,
|
|
2207
|
+
request,
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* Move a document (and its indexed chunks) to another base. With no
|
|
2213
|
+
* targetBaseId, applies the document's stored curation suggestion.
|
|
2214
|
+
*/
|
|
2215
|
+
async moveDocument(
|
|
2216
|
+
workspaceId: string,
|
|
2217
|
+
documentId: string,
|
|
2218
|
+
request: MoveDocumentRequest = {},
|
|
2219
|
+
): Promise<Document> {
|
|
2220
|
+
return await this.requestJson<Document>(
|
|
2221
|
+
"POST",
|
|
2222
|
+
`/v1/workspaces/${workspaceId}/documents/${documentId}/move`,
|
|
2223
|
+
request,
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2094
2227
|
/** Retry indexing for a failed document. */
|
|
2095
2228
|
async reindexDocument(
|
|
2096
2229
|
workspaceId: string,
|
|
@@ -2393,15 +2526,12 @@ export class OpenGeniClient {
|
|
|
2393
2526
|
|
|
2394
2527
|
// --- GitHub ----------------------------------------------------------------------------------
|
|
2395
2528
|
|
|
2396
|
-
/** GitHub App configuration
|
|
2529
|
+
/** GitHub App server configuration plus truthful workspace binding status. */
|
|
2397
2530
|
async getGitHubApp(workspaceId: string): Promise<GitHubAppInfo> {
|
|
2398
2531
|
return await this.requestJson<GitHubAppInfo>("GET", `/v1/workspaces/${workspaceId}/github/app`);
|
|
2399
2532
|
}
|
|
2400
2533
|
|
|
2401
|
-
/**
|
|
2402
|
-
* Compatibility URL for previously issued state. New installation binding is
|
|
2403
|
-
* disabled, so the endpoint validates state and terminates with HTTP 410.
|
|
2404
|
-
*/
|
|
2534
|
+
/** Build the GitHub owner-consent entry URL for fresh workspace-bound state. */
|
|
2405
2535
|
githubConnectUrl(workspaceId: string, state: string): string {
|
|
2406
2536
|
return this.url(`/v1/workspaces/${workspaceId}/github/connect`, { state });
|
|
2407
2537
|
}
|
|
@@ -2508,13 +2638,14 @@ export class OpenGeniClient {
|
|
|
2508
2638
|
|
|
2509
2639
|
// --- Internals -------------------------------------------------------------
|
|
2510
2640
|
|
|
2511
|
-
private headers(): Record<string, string> {
|
|
2641
|
+
private headers(correlationId?: string): Record<string, string> {
|
|
2512
2642
|
const extra =
|
|
2513
2643
|
typeof this.options.headers === "function" ? this.options.headers() : this.options.headers;
|
|
2514
2644
|
return {
|
|
2515
2645
|
...(this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}),
|
|
2516
2646
|
...extra,
|
|
2517
2647
|
[OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
|
|
2648
|
+
...(correlationId ? { [OPENGENI_CORRELATION_HEADER]: correlationId } : {}),
|
|
2518
2649
|
};
|
|
2519
2650
|
}
|
|
2520
2651
|
|
|
@@ -2678,37 +2809,63 @@ export class OpenGeniClient {
|
|
|
2678
2809
|
query: Record<string, string> = {},
|
|
2679
2810
|
options: OpenGeniRequestOptions = {},
|
|
2680
2811
|
): Promise<T> {
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2812
|
+
const correlationId = crypto.randomUUID();
|
|
2813
|
+
let response: Response;
|
|
2814
|
+
try {
|
|
2815
|
+
response = await this.fetchImpl(this.url(path, query), {
|
|
2816
|
+
method,
|
|
2817
|
+
headers: {
|
|
2818
|
+
...this.headers(correlationId),
|
|
2819
|
+
Accept: "application/json",
|
|
2820
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
2821
|
+
},
|
|
2822
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
2823
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
2824
|
+
});
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
if (isMutationMethod(method)) {
|
|
2827
|
+
throw mutationTransportError(correlationId);
|
|
2828
|
+
}
|
|
2829
|
+
throw error;
|
|
2830
|
+
}
|
|
2691
2831
|
assertApiContractResponse(response);
|
|
2692
2832
|
if (!response.ok) {
|
|
2693
|
-
throw
|
|
2833
|
+
throw await apiErrorFromResponse(response, { method, correlationId });
|
|
2834
|
+
}
|
|
2835
|
+
await assertJsonResponse(response, { method, correlationId });
|
|
2836
|
+
try {
|
|
2837
|
+
return (await response.json()) as T;
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
if (isMutationMethod(method)) {
|
|
2840
|
+
throw mutationTransportError(correlationId);
|
|
2841
|
+
}
|
|
2842
|
+
throw error;
|
|
2694
2843
|
}
|
|
2695
|
-
return (await response.json()) as T;
|
|
2696
2844
|
}
|
|
2697
2845
|
|
|
2698
2846
|
/** Like `requestJson` for endpoints that respond with no body (204). */
|
|
2699
2847
|
private async requestVoid(method: string, path: string, body?: unknown): Promise<void> {
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2848
|
+
const correlationId = crypto.randomUUID();
|
|
2849
|
+
let response: Response;
|
|
2850
|
+
try {
|
|
2851
|
+
response = await this.fetchImpl(this.url(path), {
|
|
2852
|
+
method,
|
|
2853
|
+
headers: {
|
|
2854
|
+
...this.headers(correlationId),
|
|
2855
|
+
Accept: "application/json",
|
|
2856
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
2857
|
+
},
|
|
2858
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
2859
|
+
});
|
|
2860
|
+
} catch (error) {
|
|
2861
|
+
if (isMutationMethod(method)) {
|
|
2862
|
+
throw mutationTransportError(correlationId);
|
|
2863
|
+
}
|
|
2864
|
+
throw error;
|
|
2865
|
+
}
|
|
2709
2866
|
assertApiContractResponse(response);
|
|
2710
2867
|
if (!response.ok) {
|
|
2711
|
-
throw
|
|
2868
|
+
throw await apiErrorFromResponse(response, { method, correlationId });
|
|
2712
2869
|
}
|
|
2713
2870
|
}
|
|
2714
2871
|
}
|
|
@@ -2720,22 +2877,86 @@ function assertApiContractResponse(response: Response): void {
|
|
|
2720
2877
|
}
|
|
2721
2878
|
}
|
|
2722
2879
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2880
|
+
const API_ERROR_MAX_BYTES = 16 * 1024;
|
|
2881
|
+
|
|
2882
|
+
type ApiErrorRequestContext = {
|
|
2883
|
+
method: string;
|
|
2884
|
+
correlationId?: string | undefined;
|
|
2885
|
+
};
|
|
2886
|
+
|
|
2887
|
+
async function apiErrorFromResponse(
|
|
2888
|
+
response: Response,
|
|
2889
|
+
context: ApiErrorRequestContext,
|
|
2890
|
+
): Promise<OpenGeniApiError> {
|
|
2891
|
+
return new OpenGeniApiError(response.status, await readBoundedJsonErrorBody(response), {
|
|
2892
|
+
correlationId: response.headers.get(OPENGENI_CORRELATION_HEADER) ?? context.correlationId,
|
|
2893
|
+
mutation: isMutationMethod(context.method),
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
async function assertJsonResponse(
|
|
2898
|
+
response: Response,
|
|
2899
|
+
context: ApiErrorRequestContext,
|
|
2900
|
+
): Promise<void> {
|
|
2901
|
+
if (isJsonContentType(response.headers.get("content-type"))) return;
|
|
2902
|
+
await cancelResponseBody(response, "unexpected non-JSON API response");
|
|
2903
|
+
throw new OpenGeniApiError(502, "", {
|
|
2904
|
+
code: "upstream_unavailable",
|
|
2905
|
+
retryable: true,
|
|
2906
|
+
correlationId: response.headers.get(OPENGENI_CORRELATION_HEADER) ?? context.correlationId,
|
|
2907
|
+
outcomeUnknown: isMutationMethod(context.method),
|
|
2908
|
+
displayMessage: "OpenGeni is temporarily unavailable — retry.",
|
|
2909
|
+
});
|
|
2729
2910
|
}
|
|
2730
2911
|
|
|
2731
|
-
async function
|
|
2912
|
+
async function readBoundedJsonErrorBody(response: Response): Promise<string> {
|
|
2913
|
+
if (!isJsonContentType(response.headers.get("content-type"))) {
|
|
2914
|
+
await cancelResponseBody(response, "discarding API error body");
|
|
2915
|
+
return "";
|
|
2916
|
+
}
|
|
2917
|
+
if (Number(response.headers.get("content-length")) > API_ERROR_MAX_BYTES) {
|
|
2918
|
+
await cancelResponseBody(response, "discarding API error body");
|
|
2919
|
+
return "";
|
|
2920
|
+
}
|
|
2732
2921
|
try {
|
|
2733
|
-
return new TextDecoder().decode(
|
|
2922
|
+
return new TextDecoder().decode(
|
|
2923
|
+
await readBoundedResponseBytes(response, API_ERROR_MAX_BYTES, null),
|
|
2924
|
+
);
|
|
2734
2925
|
} catch {
|
|
2735
2926
|
return "";
|
|
2736
2927
|
}
|
|
2737
2928
|
}
|
|
2738
2929
|
|
|
2930
|
+
function isJsonContentType(value: string | null): boolean {
|
|
2931
|
+
return /^(application\/json|[^;]+\+json)\s*(;|$)/i.test(value ?? "");
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
function isMutationMethod(method: string): boolean {
|
|
2935
|
+
return method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function mutationTransportError(correlationId: string): OpenGeniApiError {
|
|
2939
|
+
return new OpenGeniApiError(0, "", {
|
|
2940
|
+
code: "network_error",
|
|
2941
|
+
retryable: true,
|
|
2942
|
+
correlationId,
|
|
2943
|
+
outcomeUnknown: true,
|
|
2944
|
+
mutation: true,
|
|
2945
|
+
displayMessage: "OpenGeni could not confirm the request — reconcile before retrying.",
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
async function sha256ForUpload(body: Blob | ArrayBuffer | string): Promise<string> {
|
|
2950
|
+
const bytes =
|
|
2951
|
+
typeof body === "string"
|
|
2952
|
+
? new TextEncoder().encode(body)
|
|
2953
|
+
: body instanceof Blob
|
|
2954
|
+
? new Uint8Array(await body.arrayBuffer())
|
|
2955
|
+
: new Uint8Array(body);
|
|
2956
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
2957
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2739
2960
|
async function cancelResponseBody(response: Response, reason: string): Promise<void> {
|
|
2740
2961
|
await response.body?.cancel(reason).catch(() => undefined);
|
|
2741
2962
|
}
|