@parall/sdk 1.56.0 → 1.56.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +13 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +35 -29
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +35 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/wiki-changeset.d.ts +4 -0
- package/dist/wiki-changeset.d.ts.map +1 -0
- package/dist/wiki-changeset.js +11 -0
- package/dist/wiki-upload.d.ts +43 -0
- package/dist/wiki-upload.d.ts.map +1 -0
- package/dist/wiki-upload.js +87 -0
- package/package.json +1 -1
- package/src/client.ts +54 -29
- package/src/constants.ts +2 -0
- package/src/index.ts +6 -0
- package/src/types.ts +45 -13
- package/src/wiki-changeset.ts +13 -0
- package/src/wiki-upload.ts +142 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function createWikiUploadFormData(params) {
|
|
2
|
+
const form = new FormData();
|
|
3
|
+
form.append('path', params.path);
|
|
4
|
+
form.append('file', params.file);
|
|
5
|
+
if (params.message)
|
|
6
|
+
form.append('message', params.message);
|
|
7
|
+
if (params.uploadId)
|
|
8
|
+
form.append('upload_id', params.uploadId);
|
|
9
|
+
if (params.conflict)
|
|
10
|
+
form.append('conflict', params.conflict);
|
|
11
|
+
if ('contentRoute' in params && params.contentRoute) {
|
|
12
|
+
form.append('content_route', params.contentRoute);
|
|
13
|
+
}
|
|
14
|
+
return form;
|
|
15
|
+
}
|
|
16
|
+
/** Send one multipart request. XHR is used only when a browser caller asks
|
|
17
|
+
* for upload progress; fetch remains the transport everywhere else. Auth
|
|
18
|
+
* refresh and API error decoding stay in ParallClient above this boundary. */
|
|
19
|
+
export function sendMultipartRequest(options) {
|
|
20
|
+
if (options.onProgress && typeof XMLHttpRequest !== 'undefined') {
|
|
21
|
+
return multipartXHR(options, options.onProgress);
|
|
22
|
+
}
|
|
23
|
+
const timeoutSignal = AbortSignal.timeout(options.timeoutMs);
|
|
24
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
25
|
+
return fetch(options.url, {
|
|
26
|
+
method: options.method,
|
|
27
|
+
headers: options.headers,
|
|
28
|
+
body: options.body,
|
|
29
|
+
signal,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function multipartXHR(options, onProgress) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const xhr = new XMLHttpRequest();
|
|
35
|
+
let settled = false;
|
|
36
|
+
const finish = (fn) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
options.signal?.removeEventListener('abort', abortFromSignal);
|
|
41
|
+
fn();
|
|
42
|
+
};
|
|
43
|
+
const abortFromSignal = () => xhr.abort();
|
|
44
|
+
xhr.open(options.method, options.url, true);
|
|
45
|
+
xhr.timeout = options.timeoutMs;
|
|
46
|
+
for (const [name, value] of Object.entries(options.headers)) {
|
|
47
|
+
xhr.setRequestHeader(name, value);
|
|
48
|
+
}
|
|
49
|
+
xhr.upload.onprogress = (event) => {
|
|
50
|
+
try {
|
|
51
|
+
onProgress(event.loaded, event.lengthComputable ? event.total : 0);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Observer failures must not cancel an otherwise healthy upload.
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
xhr.onload = () => finish(() => {
|
|
58
|
+
const responseHeaders = new Headers();
|
|
59
|
+
for (const line of xhr
|
|
60
|
+
.getAllResponseHeaders()
|
|
61
|
+
.trim()
|
|
62
|
+
.split(/[\r\n]+/)) {
|
|
63
|
+
if (!line)
|
|
64
|
+
continue;
|
|
65
|
+
const separator = line.indexOf(':');
|
|
66
|
+
if (separator > 0) {
|
|
67
|
+
responseHeaders.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const responseBody = xhr.status === 204 || xhr.responseText === '' ? null : xhr.responseText;
|
|
71
|
+
resolve(new Response(responseBody, {
|
|
72
|
+
status: xhr.status,
|
|
73
|
+
statusText: xhr.statusText,
|
|
74
|
+
headers: responseHeaders,
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
xhr.onerror = () => finish(() => reject(new TypeError('Network request failed')));
|
|
78
|
+
xhr.ontimeout = () => finish(() => reject(new DOMException('Request timed out', 'TimeoutError')));
|
|
79
|
+
xhr.onabort = () => finish(() => reject(new DOMException('Request aborted', 'AbortError')));
|
|
80
|
+
if (options.signal?.aborted) {
|
|
81
|
+
finish(() => reject(new DOMException('Request aborted', 'AbortError')));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
options.signal?.addEventListener('abort', abortFromSignal, { once: true });
|
|
85
|
+
xhr.send(options.body);
|
|
86
|
+
});
|
|
87
|
+
}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type BrowserViewerRequestOptions, browserViewerRequestOptions } from './browser-viewer.js';
|
|
2
2
|
import { API_BASE, ENDPOINTS, WIKI_BASE } from './constants.js';
|
|
3
3
|
import { AttachmentClient } from './attachment-client.js';
|
|
4
|
+
import {
|
|
5
|
+
createWikiUploadFormData,
|
|
6
|
+
sendMultipartRequest,
|
|
7
|
+
type WikiChangesetFileUploadParams,
|
|
8
|
+
type WikiFileUploadParams,
|
|
9
|
+
type WikiFileUploadResponse,
|
|
10
|
+
} from './wiki-upload.js';
|
|
11
|
+
import { normalizeWikiChangeset } from './wiki-changeset.js';
|
|
4
12
|
import type {
|
|
5
13
|
AddTeamMemberRequest,
|
|
6
14
|
AgentClip,
|
|
@@ -94,6 +102,7 @@ import type {
|
|
|
94
102
|
CreateCommentRequest,
|
|
95
103
|
CreateExternalConnectionInput,
|
|
96
104
|
CreateExternalTriggerInput,
|
|
105
|
+
CreatePersonalApiKeyRequest,
|
|
97
106
|
CreateScheduleInput,
|
|
98
107
|
CreateSetupIntentResponse,
|
|
99
108
|
CreateTeamRequest,
|
|
@@ -176,6 +185,7 @@ import type {
|
|
|
176
185
|
OutboundRefsRequest,
|
|
177
186
|
OutboundRefsResponse,
|
|
178
187
|
PaginatedResponse,
|
|
188
|
+
PersonalApiKey,
|
|
179
189
|
PlatformConfigResponse,
|
|
180
190
|
PlatformModelsResponse,
|
|
181
191
|
PublishRegistryClipRequest,
|
|
@@ -262,7 +272,6 @@ import type {
|
|
|
262
272
|
WikiCommit,
|
|
263
273
|
WikiDiff,
|
|
264
274
|
WikiFilePreviewUrlResponse,
|
|
265
|
-
WikiFileUploadResponse,
|
|
266
275
|
WikiNodeSectionArtifact,
|
|
267
276
|
WikiOperation,
|
|
268
277
|
WikiOperationsResponse,
|
|
@@ -551,7 +560,7 @@ export class ParallClient extends AttachmentClient {
|
|
|
551
560
|
* Multipart upload variant of `request`. Same auth / refresh / error
|
|
552
561
|
* handling, but lets the caller hand us a prepared `FormData` (file +
|
|
553
562
|
* text fields) and skips the JSON content-type. Used by
|
|
554
|
-
* uploadWikiFile / uploadWikiFileToChangeset — wiki
|
|
563
|
+
* uploadWikiFile / uploadWikiFileToChangeset — wiki file uploads can
|
|
555
564
|
* hit the 100 MiB cap, so a longer 5-minute timeout is used so a
|
|
556
565
|
* 50 MiB blob on a slow connection doesn't get chopped at 15 s.
|
|
557
566
|
*/
|
|
@@ -560,6 +569,11 @@ export class ParallClient extends AttachmentClient {
|
|
|
560
569
|
path: string,
|
|
561
570
|
body: FormData,
|
|
562
571
|
retried = false,
|
|
572
|
+
opts?: {
|
|
573
|
+
signal?: AbortSignal;
|
|
574
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void;
|
|
575
|
+
timeoutMs?: number;
|
|
576
|
+
},
|
|
563
577
|
): Promise<T> {
|
|
564
578
|
if (!retried) {
|
|
565
579
|
await this.ensureFreshToken(path);
|
|
@@ -568,13 +582,17 @@ export class ParallClient extends AttachmentClient {
|
|
|
568
582
|
const { 'Content-Type': _drop, ...headers } = this.buildHeaders(path);
|
|
569
583
|
void _drop;
|
|
570
584
|
|
|
585
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
|
|
571
586
|
let res: Response;
|
|
572
587
|
try {
|
|
573
|
-
res = await
|
|
588
|
+
res = await sendMultipartRequest({
|
|
574
589
|
method,
|
|
590
|
+
url: `${this.baseUrlFor(path)}${path}`,
|
|
575
591
|
headers,
|
|
576
592
|
body,
|
|
577
|
-
|
|
593
|
+
timeoutMs,
|
|
594
|
+
signal: opts?.signal,
|
|
595
|
+
onProgress: opts?.onProgress,
|
|
578
596
|
});
|
|
579
597
|
} catch (err) {
|
|
580
598
|
throw ParallClient.normalizeFetchError(err);
|
|
@@ -586,7 +604,7 @@ export class ParallClient extends AttachmentClient {
|
|
|
586
604
|
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
587
605
|
const refreshed = await this.tryRefresh();
|
|
588
606
|
if (refreshed) {
|
|
589
|
-
return this.multipartRequest<T>(method, path, body, true);
|
|
607
|
+
return this.multipartRequest<T>(method, path, body, true, opts);
|
|
590
608
|
}
|
|
591
609
|
}
|
|
592
610
|
if (this.onTokenExpired && !isAuthPath) {
|
|
@@ -687,6 +705,22 @@ export class ParallClient extends AttachmentClient {
|
|
|
687
705
|
return this.request('DELETE', ENDPOINTS.USER_AVATAR);
|
|
688
706
|
}
|
|
689
707
|
|
|
708
|
+
// ---- Personal API keys (org-scoped; JWT session required) ----
|
|
709
|
+
|
|
710
|
+
/** Mints an org-scoped personal API key. The plaintext is returned once and never again. */
|
|
711
|
+
async createPersonalApiKey(req: CreatePersonalApiKeyRequest): Promise<ApiKey> {
|
|
712
|
+
return this.request('POST', ENDPOINTS.PERSONAL_API_KEYS, req);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async listPersonalApiKeys(): Promise<PersonalApiKey[]> {
|
|
716
|
+
const res = await this.request<{ data: PersonalApiKey[] }>('GET', ENDPOINTS.PERSONAL_API_KEYS);
|
|
717
|
+
return res.data;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async revokePersonalApiKey(keyId: string): Promise<void> {
|
|
721
|
+
return this.request('DELETE', ENDPOINTS.PERSONAL_API_KEY(keyId));
|
|
722
|
+
}
|
|
723
|
+
|
|
690
724
|
async uploadAgentAvatar(
|
|
691
725
|
orgId: string,
|
|
692
726
|
agentId: string,
|
|
@@ -2923,39 +2957,38 @@ export class ParallClient extends AttachmentClient {
|
|
|
2923
2957
|
async uploadWikiFile(
|
|
2924
2958
|
orgId: string,
|
|
2925
2959
|
wikiId: string,
|
|
2926
|
-
params:
|
|
2960
|
+
params: WikiFileUploadParams,
|
|
2927
2961
|
): Promise<WikiFileUploadResponse> {
|
|
2928
2962
|
// `POST /uploads` always writes to the wiki's default branch — the
|
|
2929
|
-
// backend's parseUpload
|
|
2963
|
+
// backend's parseUpload does not accept a `parent_ref` field, so
|
|
2930
2964
|
// surfacing one in this signature would mislead callers. To target
|
|
2931
2965
|
// a feature branch, use uploadWikiFileToChangeset.
|
|
2932
|
-
const fd =
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2966
|
+
const fd = createWikiUploadFormData(params);
|
|
2967
|
+
return this.multipartRequest('POST', ENDPOINTS.WIKI_UPLOADS(orgId, wikiId), fd, false, {
|
|
2968
|
+
signal: params.signal,
|
|
2969
|
+
onProgress: params.onProgress,
|
|
2970
|
+
});
|
|
2937
2971
|
}
|
|
2938
2972
|
|
|
2939
2973
|
/**
|
|
2940
|
-
* Upload a
|
|
2941
|
-
* author-only).
|
|
2942
|
-
*
|
|
2943
|
-
*
|
|
2974
|
+
* Upload a file into a changeset's feature branch (read scope +
|
|
2975
|
+
* author-only). The default remains binary-only; contentRoute `auto`
|
|
2976
|
+
* opts into reviewable text for the Web library upload flow. On merge the
|
|
2977
|
+
* branch contents squash into the default branch.
|
|
2944
2978
|
*/
|
|
2945
2979
|
async uploadWikiFileToChangeset(
|
|
2946
2980
|
orgId: string,
|
|
2947
2981
|
wikiId: string,
|
|
2948
2982
|
changesetId: string,
|
|
2949
|
-
params:
|
|
2983
|
+
params: WikiChangesetFileUploadParams,
|
|
2950
2984
|
): Promise<WikiFileUploadResponse> {
|
|
2951
|
-
const fd =
|
|
2952
|
-
fd.append('path', params.path);
|
|
2953
|
-
fd.append('file', params.file);
|
|
2954
|
-
if (params.message) fd.append('message', params.message);
|
|
2985
|
+
const fd = createWikiUploadFormData(params);
|
|
2955
2986
|
return this.multipartRequest(
|
|
2956
2987
|
'POST',
|
|
2957
2988
|
ENDPOINTS.WIKI_CHANGESET_FILES(orgId, wikiId, changesetId),
|
|
2958
2989
|
fd,
|
|
2990
|
+
false,
|
|
2991
|
+
{ signal: params.signal, onProgress: params.onProgress },
|
|
2959
2992
|
);
|
|
2960
2993
|
}
|
|
2961
2994
|
|
|
@@ -4405,14 +4438,6 @@ export class ParallClient extends AttachmentClient {
|
|
|
4405
4438
|
}
|
|
4406
4439
|
}
|
|
4407
4440
|
|
|
4408
|
-
function normalizeWikiChangeset(changeset: WikiChangeset): WikiChangeset {
|
|
4409
|
-
return {
|
|
4410
|
-
...changeset,
|
|
4411
|
-
changed_paths: changeset.changed_paths ?? [],
|
|
4412
|
-
file_changes: changeset.file_changes ?? [],
|
|
4413
|
-
};
|
|
4414
|
-
}
|
|
4415
|
-
|
|
4416
4441
|
export class ApiError extends Error {
|
|
4417
4442
|
extras?: Record<string, unknown>;
|
|
4418
4443
|
/** Retry-After delta seconds when the server supplies one. */
|
package/src/constants.ts
CHANGED
|
@@ -361,6 +361,8 @@ export const ENDPOINTS = {
|
|
|
361
361
|
// Users
|
|
362
362
|
USERS_ME: `${API_BASE}/users/me`,
|
|
363
363
|
USER_AVATAR: `${API_BASE}/users/me/avatar`,
|
|
364
|
+
PERSONAL_API_KEYS: `${API_BASE}/users/me/api-keys`,
|
|
365
|
+
PERSONAL_API_KEY: (keyId: string) => `${API_BASE}/users/me/api-keys/${keyId}`,
|
|
364
366
|
USER: (id: string) => `${API_BASE}/users/${id}`,
|
|
365
367
|
|
|
366
368
|
// WebSocket ticket
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,12 @@ export * from './subject.js';
|
|
|
7
7
|
export * from './task-label-types.js';
|
|
8
8
|
export * from './types.js';
|
|
9
9
|
export * from './wechat-types.js';
|
|
10
|
+
export type {
|
|
11
|
+
WikiChangesetFileUploadParams,
|
|
12
|
+
WikiFileUploadParams,
|
|
13
|
+
WikiFileUploadResponse,
|
|
14
|
+
WikiStoredAs,
|
|
15
|
+
} from './wiki-upload.js';
|
|
10
16
|
export type {
|
|
11
17
|
ParallWsOptions,
|
|
12
18
|
WsClientEventMap,
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
LabelDeletedData,
|
|
5
5
|
LabelUpdatedData,
|
|
6
6
|
} from './task-label-types.js';
|
|
7
|
+
|
|
7
8
|
export type {
|
|
8
9
|
CompletedUploadPart,
|
|
9
10
|
FileUrlResponse,
|
|
@@ -63,7 +64,11 @@ export interface Chat {
|
|
|
63
64
|
created_at: string;
|
|
64
65
|
updated_at: string;
|
|
65
66
|
archived_at?: string | null;
|
|
67
|
+
archived_by?: string | null;
|
|
66
68
|
deleted_at?: string | null;
|
|
69
|
+
deleted_by?: string | null;
|
|
70
|
+
member_count?: number;
|
|
71
|
+
my_role?: ChatMemberRole;
|
|
67
72
|
pinned: boolean;
|
|
68
73
|
hidden: boolean;
|
|
69
74
|
my_notification_level?: NotificationLevel;
|
|
@@ -705,11 +710,35 @@ export interface CreateInvitationRequest {
|
|
|
705
710
|
role?: OrgMemberRole;
|
|
706
711
|
}
|
|
707
712
|
|
|
708
|
-
// Response of
|
|
709
|
-
// returned exactly once at mint time and can never
|
|
713
|
+
// Response of API key create/regenerate (agent keys and personal keys alike).
|
|
714
|
+
// The plaintext `api_key` is returned exactly once at mint time and can never
|
|
715
|
+
// be fetched again. Personal keys additionally echo the org they are bound to.
|
|
710
716
|
export interface ApiKey {
|
|
711
717
|
id: string;
|
|
712
718
|
api_key: string;
|
|
719
|
+
org_id?: string;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// A personal API key row as listed by GET /users/me/api-keys. Personal keys
|
|
723
|
+
// are org-scoped ("org_full"): the user's permissions inside org_id only.
|
|
724
|
+
// Managed Clip keys have their own surface; the key hash never leaves the
|
|
725
|
+
// server.
|
|
726
|
+
export interface PersonalApiKey {
|
|
727
|
+
id: string;
|
|
728
|
+
user_id: string;
|
|
729
|
+
name?: string;
|
|
730
|
+
org_id: string;
|
|
731
|
+
last_used_at?: string;
|
|
732
|
+
expires_at?: string;
|
|
733
|
+
created_at: string;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export interface CreatePersonalApiKeyRequest {
|
|
737
|
+
name?: string;
|
|
738
|
+
/** The org this key is bound to; the caller must be a member. */
|
|
739
|
+
org_id: string;
|
|
740
|
+
/** RFC3339; must be in the future. Omit for a non-expiring key. */
|
|
741
|
+
expires_at?: string;
|
|
713
742
|
}
|
|
714
743
|
|
|
715
744
|
export interface CreateAgentResponse {
|
|
@@ -1644,7 +1673,9 @@ export interface Task {
|
|
|
1644
1673
|
completed_at: string | null;
|
|
1645
1674
|
canceled_at: string | null;
|
|
1646
1675
|
archived_at?: string | null;
|
|
1676
|
+
archived_by?: string | null;
|
|
1647
1677
|
deleted_at?: string | null;
|
|
1678
|
+
deleted_by?: string | null;
|
|
1648
1679
|
/**
|
|
1649
1680
|
* Per-viewer: whether the requesting user may manage OTHER members' comment
|
|
1650
1681
|
* subscriptions for this task (creator / assignee / project manager / org
|
|
@@ -1841,6 +1872,8 @@ export interface Project {
|
|
|
1841
1872
|
sort_order: number;
|
|
1842
1873
|
created_at: string;
|
|
1843
1874
|
updated_at: string;
|
|
1875
|
+
archived_at?: string | null;
|
|
1876
|
+
archived_by?: string | null;
|
|
1844
1877
|
/** The requesting user's own role, computed per request rather than stored.
|
|
1845
1878
|
* Absent means they hold no membership (direct or team-derived). */
|
|
1846
1879
|
my_role?: ProjectRole;
|
|
@@ -2382,6 +2415,10 @@ export interface WikiChangeset {
|
|
|
2382
2415
|
title: string;
|
|
2383
2416
|
message: string | null;
|
|
2384
2417
|
status: WikiChangesetStatus;
|
|
2418
|
+
/** Present on current servers; optional keeps older mocked/client data source-compatible. */
|
|
2419
|
+
head_branch?: string | null;
|
|
2420
|
+
/** Present on current servers; optional keeps older mocked/client data source-compatible. */
|
|
2421
|
+
base_commit?: string | null;
|
|
2385
2422
|
merge_commit: string | null;
|
|
2386
2423
|
file_changes: WikiFileChange[];
|
|
2387
2424
|
changed_paths: string[];
|
|
@@ -2649,6 +2686,8 @@ export interface CreateWikiChangesetRequest {
|
|
|
2649
2686
|
title: string;
|
|
2650
2687
|
message?: string;
|
|
2651
2688
|
file_changes: WikiFileChangeInput[];
|
|
2689
|
+
/** Omit for the server default (`proposed`); pass `draft` for staged uploads. */
|
|
2690
|
+
status?: 'draft' | 'proposed';
|
|
2652
2691
|
source_chat_id?: string;
|
|
2653
2692
|
source_message_id?: string;
|
|
2654
2693
|
source_run_id?: string;
|
|
@@ -2658,6 +2697,8 @@ export interface UpdateWikiChangesetRequest {
|
|
|
2658
2697
|
title?: string;
|
|
2659
2698
|
message?: string;
|
|
2660
2699
|
file_changes?: WikiFileChangeInput[];
|
|
2700
|
+
/** Author-settable transitions currently include `proposed` and `closed`. */
|
|
2701
|
+
status?: 'proposed' | 'closed';
|
|
2661
2702
|
/**
|
|
2662
2703
|
* When true, file_changes define the changeset's FULL content: the server
|
|
2663
2704
|
* resets the feature branch to the current default-branch HEAD before
|
|
@@ -2823,17 +2864,6 @@ export interface WikiBlob {
|
|
|
2823
2864
|
signed_url_expires_at?: string;
|
|
2824
2865
|
}
|
|
2825
2866
|
|
|
2826
|
-
export type WikiStoredAs = 'git_blob' | 'lfs_pointer';
|
|
2827
|
-
|
|
2828
|
-
/** Response from POST /uploads or POST /changesets/{csId}/files. */
|
|
2829
|
-
export interface WikiFileUploadResponse {
|
|
2830
|
-
path: string;
|
|
2831
|
-
size: number;
|
|
2832
|
-
stored_as: WikiStoredAs;
|
|
2833
|
-
commit_sha: string;
|
|
2834
|
-
content_sha: string;
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
2867
|
/** Response from POST /files/preview-url — short-lived signed URL for
|
|
2838
2868
|
* browser `<img>`/`<video>`/`<iframe>` src. TTL is 5 minutes by default. */
|
|
2839
2869
|
export interface WikiFilePreviewUrlResponse {
|
|
@@ -4152,6 +4182,8 @@ export interface ResolvedRef {
|
|
|
4152
4182
|
type: string;
|
|
4153
4183
|
exists: boolean;
|
|
4154
4184
|
restricted?: boolean;
|
|
4185
|
+
/** The target could not be resolved because a dependency failed temporarily. */
|
|
4186
|
+
unavailable?: boolean;
|
|
4155
4187
|
|
|
4156
4188
|
// User fields
|
|
4157
4189
|
display_name?: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WikiChangeset } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Normalize fields that older wiki-service responses may omit. */
|
|
4
|
+
export function normalizeWikiChangeset(changeset: WikiChangeset): WikiChangeset {
|
|
5
|
+
return {
|
|
6
|
+
...changeset,
|
|
7
|
+
head_branch: changeset.head_branch ?? null,
|
|
8
|
+
base_commit: changeset.base_commit ?? null,
|
|
9
|
+
merge_commit: changeset.merge_commit ?? null,
|
|
10
|
+
changed_paths: changeset.changed_paths ?? [],
|
|
11
|
+
file_changes: changeset.file_changes ?? [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
interface WikiFileUploadParamsBase {
|
|
2
|
+
path: string;
|
|
3
|
+
file: Blob;
|
|
4
|
+
message?: string;
|
|
5
|
+
/** Stable per-file retry key; replays return the original successful path. */
|
|
6
|
+
uploadId?: string;
|
|
7
|
+
/** Preserve the legacy 409 by default, or atomically choose `name (N).ext`. */
|
|
8
|
+
conflict?: 'error' | 'rename';
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type WikiStoredAs = 'git_blob' | 'lfs_pointer';
|
|
14
|
+
|
|
15
|
+
/** Response from POST /uploads or POST /changesets/{csId}/files. */
|
|
16
|
+
export interface WikiFileUploadResponse {
|
|
17
|
+
path: string;
|
|
18
|
+
size: number;
|
|
19
|
+
stored_as: WikiStoredAs;
|
|
20
|
+
commit_sha: string;
|
|
21
|
+
content_sha: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parameters for the maintain-only direct upload endpoint. */
|
|
25
|
+
export type WikiFileUploadParams = WikiFileUploadParamsBase;
|
|
26
|
+
|
|
27
|
+
/** Parameters for a changeset-scoped multipart upload. */
|
|
28
|
+
export interface WikiChangesetFileUploadParams extends WikiFileUploadParamsBase {
|
|
29
|
+
/** `auto` accepts reviewable text as a Git blob on the changeset branch. */
|
|
30
|
+
contentRoute?: 'binary' | 'auto';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createWikiUploadFormData(
|
|
34
|
+
params: WikiFileUploadParams | WikiChangesetFileUploadParams,
|
|
35
|
+
): FormData {
|
|
36
|
+
const form = new FormData();
|
|
37
|
+
form.append('path', params.path);
|
|
38
|
+
form.append('file', params.file);
|
|
39
|
+
if (params.message) form.append('message', params.message);
|
|
40
|
+
if (params.uploadId) form.append('upload_id', params.uploadId);
|
|
41
|
+
if (params.conflict) form.append('conflict', params.conflict);
|
|
42
|
+
if ('contentRoute' in params && params.contentRoute) {
|
|
43
|
+
form.append('content_route', params.contentRoute);
|
|
44
|
+
}
|
|
45
|
+
return form;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface MultipartTransportOptions {
|
|
49
|
+
method: string;
|
|
50
|
+
url: string;
|
|
51
|
+
headers: Record<string, string>;
|
|
52
|
+
body: FormData;
|
|
53
|
+
timeoutMs: number;
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Send one multipart request. XHR is used only when a browser caller asks
|
|
59
|
+
* for upload progress; fetch remains the transport everywhere else. Auth
|
|
60
|
+
* refresh and API error decoding stay in ParallClient above this boundary. */
|
|
61
|
+
export function sendMultipartRequest(options: MultipartTransportOptions): Promise<Response> {
|
|
62
|
+
if (options.onProgress && typeof XMLHttpRequest !== 'undefined') {
|
|
63
|
+
return multipartXHR(options, options.onProgress);
|
|
64
|
+
}
|
|
65
|
+
const timeoutSignal = AbortSignal.timeout(options.timeoutMs);
|
|
66
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
67
|
+
return fetch(options.url, {
|
|
68
|
+
method: options.method,
|
|
69
|
+
headers: options.headers,
|
|
70
|
+
body: options.body,
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function multipartXHR(
|
|
76
|
+
options: MultipartTransportOptions,
|
|
77
|
+
onProgress: (uploadedBytes: number, totalBytes: number) => void,
|
|
78
|
+
): Promise<Response> {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const xhr = new XMLHttpRequest();
|
|
81
|
+
let settled = false;
|
|
82
|
+
|
|
83
|
+
const finish = (fn: () => void) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
options.signal?.removeEventListener('abort', abortFromSignal);
|
|
87
|
+
fn();
|
|
88
|
+
};
|
|
89
|
+
const abortFromSignal = () => xhr.abort();
|
|
90
|
+
|
|
91
|
+
xhr.open(options.method, options.url, true);
|
|
92
|
+
xhr.timeout = options.timeoutMs;
|
|
93
|
+
for (const [name, value] of Object.entries(options.headers)) {
|
|
94
|
+
xhr.setRequestHeader(name, value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
xhr.upload.onprogress = (event) => {
|
|
98
|
+
try {
|
|
99
|
+
onProgress(event.loaded, event.lengthComputable ? event.total : 0);
|
|
100
|
+
} catch {
|
|
101
|
+
// Observer failures must not cancel an otherwise healthy upload.
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
xhr.onload = () =>
|
|
105
|
+
finish(() => {
|
|
106
|
+
const responseHeaders = new Headers();
|
|
107
|
+
for (const line of xhr
|
|
108
|
+
.getAllResponseHeaders()
|
|
109
|
+
.trim()
|
|
110
|
+
.split(/[\r\n]+/)) {
|
|
111
|
+
if (!line) continue;
|
|
112
|
+
const separator = line.indexOf(':');
|
|
113
|
+
if (separator > 0) {
|
|
114
|
+
responseHeaders.append(
|
|
115
|
+
line.slice(0, separator).trim(),
|
|
116
|
+
line.slice(separator + 1).trim(),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const responseBody =
|
|
121
|
+
xhr.status === 204 || xhr.responseText === '' ? null : xhr.responseText;
|
|
122
|
+
resolve(
|
|
123
|
+
new Response(responseBody, {
|
|
124
|
+
status: xhr.status,
|
|
125
|
+
statusText: xhr.statusText,
|
|
126
|
+
headers: responseHeaders,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
xhr.onerror = () => finish(() => reject(new TypeError('Network request failed')));
|
|
131
|
+
xhr.ontimeout = () =>
|
|
132
|
+
finish(() => reject(new DOMException('Request timed out', 'TimeoutError')));
|
|
133
|
+
xhr.onabort = () => finish(() => reject(new DOMException('Request aborted', 'AbortError')));
|
|
134
|
+
|
|
135
|
+
if (options.signal?.aborted) {
|
|
136
|
+
finish(() => reject(new DOMException('Request aborted', 'AbortError')));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
options.signal?.addEventListener('abort', abortFromSignal, { once: true });
|
|
140
|
+
xhr.send(options.body);
|
|
141
|
+
});
|
|
142
|
+
}
|