@unisource/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -1,233 +1,472 @@
1
- import type { ApiError } from './primitives';
2
-
3
- // ─── SDK Error classes ────────────────────────────────────────────────────────
4
-
5
- export class UnisourceError extends Error {
6
- constructor(
7
- message: string,
8
- public readonly status: number,
9
- public readonly body: ApiError
10
- ) {
11
- super(message);
12
- this.name = 'UnisourceError';
13
- }
14
- }
15
-
16
- export class UnisourceNetworkError extends Error {
17
- constructor(
18
- message: string,
19
- public readonly cause: unknown
20
- ) {
21
- super(message);
22
- this.name = 'UnisourceNetworkError';
23
- }
24
- }
25
-
26
- // ─── Config ───────────────────────────────────────────────────────────────────
27
-
28
- export interface UnisourceClientConfig {
29
- /** Base URL of the UniSource API, e.g. https://api.usrc.dev */
30
- baseUrl: string;
31
- /** Service identifier — tells the backend which service this client belongs to */
32
- serviceId: string;
33
- /**
34
- * Returns a fresh JWT or API key for each request.
35
- * Return null/undefined to send unauthenticated requests.
36
- */
37
- getToken: () => string | null | undefined | Promise<string | null | undefined>;
38
- }
39
-
40
- // ─── Internal fetch helper ────────────────────────────────────────────────────
41
-
42
- async function apiRequest<T>(
43
- config: UnisourceClientConfig,
44
- method: string,
45
- path: string,
46
- options: { body?: unknown; query?: Record<string, string | number | boolean | undefined | null> } = {}
47
- ): Promise<T> {
48
- const token = await config.getToken();
49
-
50
- const url = new URL(path, config.baseUrl);
51
- if (options.query) {
52
- for (const [key, value] of Object.entries(options.query)) {
53
- if (value !== undefined && value !== null) {
54
- url.searchParams.set(key, String(value));
55
- }
56
- }
57
- }
58
-
59
- const headers: Record<string, string> = {
60
- 'X-Service-ID': config.serviceId,
61
- };
62
- if (token) {
63
- headers['Authorization'] = `Bearer ${token}`;
64
- }
65
- if (options.body !== undefined) {
66
- headers['Content-Type'] = 'application/json';
67
- }
68
-
69
- let response: Response;
70
- try {
71
- response = await fetch(url.toString(), {
72
- method,
73
- headers,
74
- body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
75
- });
76
- } catch (err) {
77
- throw new UnisourceNetworkError('Network request failed', err);
78
- }
79
-
80
- if (!response.ok) {
81
- let body: ApiError;
82
- try {
83
- body = (await response.json()) as ApiError;
84
- } catch {
85
- body = { error: 'Unknown', message: response.statusText };
86
- }
87
- throw new UnisourceError(body.message, response.status, body);
88
- }
89
-
90
- return response.json() as Promise<T>;
91
- }
92
-
93
- // ─── Client class ─────────────────────────────────────────────────────────────
94
-
95
- import type {
96
- UploadR2InitRequest,
97
- UploadR2InitResponse,
98
- UploadAppwriteInitRequest,
99
- UploadAppwriteInitResponse,
100
- UploadLifecycleRequest,
101
- UploadCompleteResponse,
102
- UploadFailResponse,
103
- UploadsListResponse,
104
- } from './uploads';
105
-
106
- import type {
107
- FileRecord,
108
- FileRecordsListQuery,
109
- FileRecordsListResponse,
110
- FileRecordDetailResponse,
111
- FileMoveRequest,
112
- FileDownloadUrlResponse,
113
- FileDeleteResponse,
114
- FileRestoreResponse,
115
- } from './fileRecords';
116
-
117
- import type {
118
- FolderListQuery,
119
- FolderListResponse,
120
- FolderCreateRequest,
121
- FolderCreateResponse,
122
- FolderUpdateRequest,
123
- FolderUpdateResponse,
124
- FolderDeleteResponse,
125
- FolderRestoreResponse,
126
- } from './folders';
127
-
128
- import type {
129
- ServiceDetailResponse,
130
- ServiceUsageResponse,
131
- AuditLogListQuery,
132
- AuditLogListResponse,
133
- } from './services';
134
-
135
- export class UnisourceClient {
136
- private config: UnisourceClientConfig;
137
-
138
- constructor(config: UnisourceClientConfig) {
139
- this.config = config;
140
- }
141
-
142
- // ─── Upload ────────────────────────────────────────────────────────────────
143
-
144
- readonly upload = {
145
- /** Initiate an R2 upload — returns a presigned PUT URL */
146
- r2Init: (body: UploadR2InitRequest): Promise<UploadR2InitResponse> =>
147
- apiRequest(this.config, 'POST', '/upload/r2/init', { body }),
148
-
149
- /** Initiate an Appwrite upload returns credentials for direct SDK upload */
150
- appwriteInit: (body: UploadAppwriteInitRequest): Promise<UploadAppwriteInitResponse> =>
151
- apiRequest(this.config, 'POST', '/upload/appwrite/init', { body }),
152
-
153
- /** Confirm that the file was successfully uploaded to storage */
154
- complete: (body: UploadLifecycleRequest): Promise<UploadCompleteResponse> =>
155
- apiRequest(this.config, 'POST', '/upload/complete', { body }),
156
-
157
- /** Mark an upload as failed and release reserved quota */
158
- fail: (body: UploadLifecycleRequest): Promise<UploadFailResponse> =>
159
- apiRequest(this.config, 'POST', '/upload/fail', { body }),
160
- };
161
-
162
- // ─── My Files ─────────────────────────────────────────────────────────────
163
-
164
- readonly myFiles = {
165
- /** List files owned by the authenticated user */
166
- list: (query?: FileRecordsListQuery): Promise<FileRecordsListResponse> =>
167
- apiRequest(this.config, 'GET', '/my-files', { query }),
168
-
169
- /** Get a single file record */
170
- get: (id: string): Promise<FileRecordDetailResponse> =>
171
- apiRequest(this.config, 'GET', `/my-files/${id}`),
172
-
173
- /** Get a time-limited download URL (never cached by proxy) */
174
- downloadUrl: (id: string): Promise<FileDownloadUrlResponse> =>
175
- apiRequest(this.config, 'GET', `/my-files/${id}/download-url`),
176
-
177
- /** Move file to a different folder (null = root) */
178
- move: (id: string, body: FileMoveRequest): Promise<{ file: FileRecord }> =>
179
- apiRequest(this.config, 'PATCH', `/my-files/${id}/move`, { body }),
180
-
181
- /** Soft-delete (move to trash) or permanently delete */
182
- delete: (id: string, query?: { permanent?: boolean }): Promise<FileDeleteResponse> =>
183
- apiRequest(this.config, 'DELETE', `/my-files/${id}`, { query }),
184
-
185
- /** Restore a file from trash */
186
- restore: (id: string): Promise<FileRestoreResponse> =>
187
- apiRequest(this.config, 'POST', `/my-files/${id}/restore`),
188
- };
189
-
190
- // ─── Folders ──────────────────────────────────────────────────────────────
191
-
192
- readonly folders = {
193
- /** List folders owned by the authenticated user */
194
- list: (query?: FolderListQuery): Promise<FolderListResponse> =>
195
- apiRequest(this.config, 'GET', '/folders', { query }),
196
-
197
- /** Create a new folder */
198
- create: (body: FolderCreateRequest): Promise<FolderCreateResponse> =>
199
- apiRequest(this.config, 'POST', '/folders', { body }),
200
-
201
- /** Update folder name or color tag */
202
- update: (id: string, body: FolderUpdateRequest): Promise<FolderUpdateResponse> =>
203
- apiRequest(this.config, 'PATCH', `/folders/${id}`, { body }),
204
-
205
- /** Soft-delete or permanently delete a folder and its contents */
206
- delete: (id: string, query?: { permanent?: boolean }): Promise<FolderDeleteResponse> =>
207
- apiRequest(this.config, 'DELETE', `/folders/${id}`, { query }),
208
-
209
- /** Restore a folder from trash */
210
- restore: (id: string): Promise<FolderRestoreResponse> =>
211
- apiRequest(this.config, 'POST', `/folders/${id}/restore`),
212
- };
213
-
214
- // ─── Admin ────────────────────────────────────────────────────────────────
215
-
216
- readonly admin = {
217
- /** Get service info and quota limits */
218
- serviceDetail: (): Promise<ServiceDetailResponse> =>
219
- apiRequest(this.config, 'GET', '/admin/service'),
220
-
221
- /** Get real-time storage usage for the service */
222
- usage: (): Promise<ServiceUsageResponse> =>
223
- apiRequest(this.config, 'GET', '/admin/service/usage'),
224
-
225
- /** List all uploads (pending/completed/failed) for this service */
226
- listUploads: (query?: { status?: string; cursor?: string; limit?: number }): Promise<UploadsListResponse> =>
227
- apiRequest(this.config, 'GET', '/files', { query }),
228
-
229
- /** List audit log events for this service */
230
- auditLog: (query?: AuditLogListQuery): Promise<AuditLogListResponse> =>
231
- apiRequest(this.config, 'GET', '/admin/audit-log', { query }),
232
- };
233
- }
1
+ import type { ApiError, UploadStatus } from './primitives';
2
+ import type {
3
+ UploadR2InitRequest,
4
+ UploadR2InitResponse,
5
+ UploadAppwriteInitRequest,
6
+ UploadAppwriteInitResponse,
7
+ UploadLifecycleRequest,
8
+ UploadCompleteResponse,
9
+ UploadFailResponse,
10
+ UploadsListResponse,
11
+ UploadRecordDetailResponse,
12
+ } from './uploads';
13
+ import type {
14
+ FileRecord,
15
+ FileRecordsListQuery,
16
+ FileRecordsListResponse,
17
+ FileRecordDetailResponse,
18
+ FileMoveRequest,
19
+ FileDownloadUrlResponse,
20
+ FileDeleteResponse,
21
+ FileRestoreResponse,
22
+ FileUpdateRequest,
23
+ FileUpdateResponse,
24
+ } from './fileRecords';
25
+ import type {
26
+ FolderListQuery,
27
+ FolderListResponse,
28
+ FolderDetailResponse,
29
+ FolderCreateRequest,
30
+ FolderCreateResponse,
31
+ FolderUpdateRequest,
32
+ FolderUpdateResponse,
33
+ FolderDeleteResponse,
34
+ FolderRestoreResponse,
35
+ } from './folders';
36
+ import type {
37
+ ServiceDetailResponse,
38
+ ServiceUsageResponse,
39
+ AdminServiceUpdateRequest,
40
+ AdminServiceUpdateResponse,
41
+ AuditLogListQuery,
42
+ AuditLogListResponse,
43
+ AdminUserListResponse,
44
+ AdminUserUpdateRequest,
45
+ AdminUserUpdateResponse,
46
+ AdminUserPasswordResetRequest,
47
+ AdminUserPasswordResetResponse,
48
+ } from './services';
49
+ import type {
50
+ ShareLinkCreateRequest,
51
+ ShareLinkCreateResponse,
52
+ ShareLinkListResponse,
53
+ PublicFileAccessResponse,
54
+ PublicFileLockedResponse,
55
+ ShareLinkUpdateRequest,
56
+ ShareLinkUpdateResponse,
57
+ ShareLinkDeleteResponse,
58
+ } from './shareLinks';
59
+ import type {
60
+ MainStorageListQuery,
61
+ MainStorageListResponse,
62
+ MainStorageDeleteResponse,
63
+ MainStorageRestoreResponse,
64
+ } from './mainStorage';
65
+ import type {
66
+ ReleaseDTO,
67
+ ReleaseUploadInitRequest,
68
+ ReleaseUploadInitResponse,
69
+ ReleaseUploadCompleteRequest,
70
+ ReleaseUploadCompleteResponse,
71
+ ReleaseUploadFailResponse,
72
+ ReleasesListQuery,
73
+ ReleasesListResponse,
74
+ ReleaseUpdateRequest,
75
+ ReleaseDeleteResponse,
76
+ ReleaseSyncRequest,
77
+ ReleaseSyncResponse,
78
+ } from './releases';
79
+
80
+ // ─── SDK Error classes ────────────────────────────────────────────────────────
81
+
82
+ export class UnisourceError extends Error {
83
+ constructor(
84
+ message: string,
85
+ public readonly status: number,
86
+ public readonly body: ApiError
87
+ ) {
88
+ super(message);
89
+ this.name = 'UnisourceError';
90
+ }
91
+ }
92
+
93
+ export class UnisourceNetworkError extends Error {
94
+ constructor(
95
+ message: string,
96
+ public readonly cause: unknown
97
+ ) {
98
+ super(message);
99
+ this.name = 'UnisourceNetworkError';
100
+ }
101
+ }
102
+
103
+ // ─── Config ───────────────────────────────────────────────────────────────────
104
+
105
+ export interface UnisourceClientConfig {
106
+ /** Base URL of the UniSource API, e.g. https://api.usrc.dev */
107
+ baseUrl: string;
108
+ /** Service identifier — tells the backend which service this client belongs to */
109
+ serviceId: string;
110
+ /**
111
+ * Returns a fresh JWT or API key for each request.
112
+ * Return null/undefined to send unauthenticated requests.
113
+ */
114
+ getToken: () => string | null | undefined | Promise<string | null | undefined>;
115
+ }
116
+
117
+ // ─── Internal fetch helper ────────────────────────────────────────────────────
118
+
119
+ async function fetchApi<T>(
120
+ baseUrl: string,
121
+ method: string,
122
+ path: string,
123
+ options: {
124
+ body?: unknown;
125
+ query?: Record<string, string | number | boolean | undefined | null>;
126
+ signal?: AbortSignal;
127
+ authHeaders?: Record<string, string>;
128
+ } = {}
129
+ ): Promise<T> {
130
+ const url = new URL(path, baseUrl);
131
+ if (options.query) {
132
+ for (const [key, value] of Object.entries(options.query)) {
133
+ if (value !== undefined && value !== null) {
134
+ url.searchParams.set(key, String(value));
135
+ }
136
+ }
137
+ }
138
+
139
+ const headers: Record<string, string> = { ...options.authHeaders };
140
+ if (options.body !== undefined) {
141
+ headers['Content-Type'] = 'application/json';
142
+ }
143
+
144
+ let response: Response;
145
+ try {
146
+ response = await fetch(url.toString(), {
147
+ method,
148
+ headers,
149
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
150
+ signal: options.signal,
151
+ });
152
+ } catch (err) {
153
+ throw new UnisourceNetworkError('Network request failed', err);
154
+ }
155
+
156
+ if (!response.ok) {
157
+ let body: ApiError;
158
+ try {
159
+ body = (await response.json()) as ApiError;
160
+ } catch {
161
+ body = { error: 'Unknown', message: response.statusText };
162
+ }
163
+ throw new UnisourceError(body.message, response.status, body);
164
+ }
165
+
166
+ return response.json() as Promise<T>;
167
+ }
168
+
169
+ async function apiRequest<T>(
170
+ config: UnisourceClientConfig,
171
+ method: string,
172
+ path: string,
173
+ options: { body?: unknown; query?: Record<string, string | number | boolean | undefined | null>; signal?: AbortSignal; extraHeaders?: Record<string, string> } = {}
174
+ ): Promise<T> {
175
+ const token = await config.getToken();
176
+ const authHeaders: Record<string, string> = {
177
+ ...(options.extraHeaders ?? {}),
178
+ 'X-Service-ID': config.serviceId,
179
+ };
180
+ if (token) {
181
+ authHeaders['Authorization'] = `Bearer ${token}`;
182
+ }
183
+ return fetchApi<T>(config.baseUrl, method, path, { ...options, authHeaders });
184
+ }
185
+
186
+ // ─── Client class ─────────────────────────────────────────────────────────────
187
+
188
+ export async function getPublicFileInfo(
189
+ baseUrl: string,
190
+ slug: string,
191
+ signal?: AbortSignal
192
+ ): Promise<PublicFileAccessResponse | PublicFileLockedResponse> {
193
+ return fetchApi(baseUrl, 'GET', `/public/${encodeURIComponent(slug)}`, { signal });
194
+ }
195
+
196
+ export async function unlockPublicFile(
197
+ baseUrl: string,
198
+ slug: string,
199
+ password: string,
200
+ signal?: AbortSignal
201
+ ): Promise<PublicFileAccessResponse | PublicFileLockedResponse> {
202
+ return fetchApi(baseUrl, 'POST', `/public/${encodeURIComponent(slug)}/unlock`, {
203
+ body: { password },
204
+ signal,
205
+ });
206
+ }
207
+
208
+ export class UnisourceClient {
209
+ private config: UnisourceClientConfig;
210
+
211
+ constructor(config: UnisourceClientConfig) {
212
+ this.config = config;
213
+ }
214
+
215
+ private request<T>(
216
+ method: string,
217
+ path: string,
218
+ options: { body?: unknown; query?: Record<string, string | number | boolean | undefined | null>; signal?: AbortSignal; extraHeaders?: Record<string, string> } = {}
219
+ ): Promise<T> {
220
+ return apiRequest<T>(this.config, method, path, options);
221
+ }
222
+
223
+ private withAsUser(options?: { asUser?: string }): Record<string, string> {
224
+ return options?.asUser ? { 'X-Target-User-ID': options.asUser } : {};
225
+ }
226
+
227
+ // ─── Upload ────────────────────────────────────────────────────────────────
228
+
229
+ readonly upload = {
230
+ /** Initiate an R2 upload — returns a presigned PUT URL */
231
+ r2Init: (body: UploadR2InitRequest, signal?: AbortSignal): Promise<UploadR2InitResponse> =>
232
+ apiRequest(this.config, 'POST', '/upload/r2/init', { body, signal }),
233
+
234
+ /** Initiate an Appwrite upload — returns credentials for direct SDK upload */
235
+ appwriteInit: (body: UploadAppwriteInitRequest, signal?: AbortSignal): Promise<UploadAppwriteInitResponse> =>
236
+ apiRequest(this.config, 'POST', '/upload/appwrite/init', { body, signal }),
237
+
238
+ /** Confirm that the file was successfully uploaded to storage */
239
+ complete: (body: UploadLifecycleRequest, signal?: AbortSignal): Promise<UploadCompleteResponse> =>
240
+ apiRequest(this.config, 'POST', '/upload/complete', { body, signal }),
241
+
242
+ /** Mark an upload as failed and release reserved quota */
243
+ fail: (body: UploadLifecycleRequest, signal?: AbortSignal): Promise<UploadFailResponse> =>
244
+ apiRequest(this.config, 'POST', '/upload/fail', { body, signal }),
245
+ };
246
+
247
+ // ─── My Files ─────────────────────────────────────────────────────────────
248
+
249
+ readonly myFiles = {
250
+ /** List files owned by the authenticated user */
251
+ list: (query?: FileRecordsListQuery, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileRecordsListResponse> =>
252
+ apiRequest(this.config, 'GET', '/my-files', { query, signal, extraHeaders: this.withAsUser(options) }),
253
+
254
+ /** List files in the trash */
255
+ trash: (query?: { cursor?: string; limit?: number }, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileRecordsListResponse> =>
256
+ apiRequest(this.config, 'GET', '/my-files/trash', { query, signal, extraHeaders: this.withAsUser(options) }),
257
+
258
+ /** Get a single file record */
259
+ get: (id: string, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileRecordDetailResponse> =>
260
+ apiRequest(this.config, 'GET', `/my-files/${id}`, { signal, extraHeaders: this.withAsUser(options) }),
261
+
262
+ /** Get a time-limited download URL (never cached by proxy) */
263
+ downloadUrl: (id: string, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileDownloadUrlResponse> =>
264
+ apiRequest(this.config, 'GET', `/my-files/${id}/download-url`, { signal, extraHeaders: this.withAsUser(options) }),
265
+
266
+ /** Move file to a different folder (null = root) */
267
+ move: (id: string, body: FileMoveRequest, signal?: AbortSignal, options?: { asUser?: string }): Promise<{ file: FileRecord }> =>
268
+ apiRequest(this.config, 'PATCH', `/my-files/${id}/move`, { body, signal, extraHeaders: this.withAsUser(options) }),
269
+
270
+ /** Soft-delete (move to trash) or permanently delete */
271
+ delete: (id: string, query?: { permanent?: boolean }, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileDeleteResponse> =>
272
+ apiRequest(this.config, 'DELETE', `/my-files/${id}`, { query, signal, extraHeaders: this.withAsUser(options) }),
273
+
274
+ /** Restore a file from trash */
275
+ restore: (id: string, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileRestoreResponse> =>
276
+ apiRequest(this.config, 'POST', `/my-files/${id}/restore`, { signal, extraHeaders: this.withAsUser(options) }),
277
+
278
+ /** Rename a file */
279
+ update: (id: string, body: FileUpdateRequest, signal?: AbortSignal, options?: { asUser?: string }): Promise<FileUpdateResponse> =>
280
+ apiRequest(this.config, 'PATCH', `/my-files/${id}`, { body, signal, extraHeaders: this.withAsUser(options) }),
281
+ };
282
+
283
+ // ─── Folders ──────────────────────────────────────────────────────────────
284
+
285
+ readonly folders = {
286
+ /** List folders owned by the authenticated user */
287
+ list: (query?: FolderListQuery, signal?: AbortSignal, options?: { asUser?: string }): Promise<FolderListResponse> =>
288
+ apiRequest(this.config, 'GET', '/folders', { query, signal, extraHeaders: this.withAsUser(options) }),
289
+
290
+ /** Get a single folder by ID */
291
+ get: (id: string, signal?: AbortSignal, options?: { asUser?: string }): Promise<FolderDetailResponse> =>
292
+ apiRequest(this.config, 'GET', `/folders/${id}`, { signal, extraHeaders: this.withAsUser(options) }),
293
+
294
+ /** Create a new folder */
295
+ create: (body: FolderCreateRequest, signal?: AbortSignal, options?: { asUser?: string }): Promise<FolderCreateResponse> =>
296
+ apiRequest(this.config, 'POST', '/folders', { body, signal, extraHeaders: this.withAsUser(options) }),
297
+
298
+ /** Update folder name or color tag */
299
+ update: (id: string, body: FolderUpdateRequest, signal?: AbortSignal, options?: { asUser?: string }): Promise<FolderUpdateResponse> =>
300
+ apiRequest(this.config, 'PATCH', `/folders/${id}`, { body, signal, extraHeaders: this.withAsUser(options) }),
301
+
302
+ /** Soft-delete or permanently delete a folder and its contents */
303
+ delete: (id: string, query?: { permanent?: boolean }, signal?: AbortSignal, options?: { asUser?: string }): Promise<FolderDeleteResponse> =>
304
+ apiRequest(this.config, 'DELETE', `/folders/${id}`, { query, signal, extraHeaders: this.withAsUser(options) }),
305
+
306
+ /** Restore a folder from trash */
307
+ restore: (id: string, signal?: AbortSignal, options?: { asUser?: string }): Promise<FolderRestoreResponse> =>
308
+ apiRequest(this.config, 'POST', `/folders/${id}/restore`, { signal, extraHeaders: this.withAsUser(options) }),
309
+ };
310
+
311
+ // ─── Main Storage ─────────────────────────────────────────────────────────
312
+
313
+ readonly mainStorage = {
314
+ /** List files in the main storage pool */
315
+ list: (query?: MainStorageListQuery): Promise<MainStorageListResponse> =>
316
+ this.request('GET', '/main', { query }),
317
+
318
+ /** Get a single main-storage file record */
319
+ get: (fileId: string): Promise<FileRecord> =>
320
+ this.request('GET', `/main/${fileId}`),
321
+
322
+ /** Rename a main-storage file */
323
+ rename: (fileId: string, filename: string): Promise<FileRecord> =>
324
+ this.request('PATCH', `/main/${fileId}`, { body: { filename } }),
325
+
326
+ /** Delete a main-storage file (soft by default; pass permanent=true for hard delete) */
327
+ delete: (fileId: string, permanent = false): Promise<MainStorageDeleteResponse> =>
328
+ this.request('DELETE', `/main/${fileId}${permanent ? '?permanent=true' : ''}`),
329
+
330
+ /** Restore a soft-deleted main-storage file */
331
+ restore: (fileId: string): Promise<MainStorageRestoreResponse> =>
332
+ this.request('POST', `/main/${fileId}/restore`),
333
+
334
+ upload: {
335
+ /** Initiate an R2 upload targeting main storage */
336
+ r2Init: (input: Omit<UploadR2InitRequest, 'is_main_storage'>): Promise<UploadR2InitResponse> =>
337
+ this.request('POST', '/upload/r2/init', { body: { ...input, is_main_storage: true } }),
338
+
339
+ /** Initiate an Appwrite upload targeting main storage */
340
+ appwriteInit: (input: Omit<UploadAppwriteInitRequest, 'is_main_storage'>): Promise<UploadAppwriteInitResponse> =>
341
+ this.request('POST', '/upload/appwrite/init', { body: { ...input, is_main_storage: true } }),
342
+
343
+ /** Confirm a main-storage upload completed */
344
+ complete: (uploadId: string): Promise<UploadCompleteResponse> =>
345
+ this.request('POST', '/upload/complete', { body: { upload_id: uploadId, is_main_storage: true } }),
346
+
347
+ /** Mark a main-storage upload as failed */
348
+ fail: (uploadId: string): Promise<UploadFailResponse> =>
349
+ this.request('POST', '/upload/fail', { body: { upload_id: uploadId } }),
350
+ },
351
+ };
352
+
353
+ // ─── Releases ─────────────────────────────────────────────────────────────
354
+
355
+ readonly releases = {
356
+ upload: {
357
+ /** Initiate a release upload — returns a presigned PUT URL */
358
+ init: (body: ReleaseUploadInitRequest, signal?: AbortSignal): Promise<ReleaseUploadInitResponse> =>
359
+ apiRequest(this.config, 'POST', '/releases/upload/init', { body, signal }),
360
+
361
+ /** Confirm that a release object was successfully uploaded */
362
+ complete: (body: ReleaseUploadCompleteRequest, signal?: AbortSignal): Promise<ReleaseUploadCompleteResponse> =>
363
+ apiRequest(this.config, 'POST', '/releases/upload/complete', { body, signal }),
364
+
365
+ /** Mark a release upload as failed */
366
+ fail: (releaseId: string, signal?: AbortSignal): Promise<ReleaseUploadFailResponse> =>
367
+ apiRequest(this.config, 'POST', '/releases/upload/fail', { body: { release_id: releaseId }, signal }),
368
+ },
369
+
370
+ /** List releases for the configured service */
371
+ list: (query?: ReleasesListQuery, signal?: AbortSignal): Promise<ReleasesListResponse> =>
372
+ apiRequest(this.config, 'GET', '/releases', { query, signal }),
373
+
374
+ /** Get a single release */
375
+ get: (releaseId: string, signal?: AbortSignal): Promise<ReleaseDTO> =>
376
+ apiRequest(this.config, 'GET', `/releases/${releaseId}`, { signal }),
377
+
378
+ /** Get the latest completed release */
379
+ latest: (signal?: AbortSignal): Promise<ReleaseDTO> =>
380
+ apiRequest(this.config, 'GET', '/releases/latest', { signal }),
381
+
382
+ /** Update release metadata */
383
+ update: (releaseId: string, body: ReleaseUpdateRequest, signal?: AbortSignal): Promise<ReleaseDTO> =>
384
+ apiRequest(this.config, 'PATCH', `/releases/${releaseId}`, { body, signal }),
385
+
386
+ /** Delete a release and its completed object, when applicable */
387
+ delete: (releaseId: string, signal?: AbortSignal): Promise<ReleaseDeleteResponse> =>
388
+ apiRequest(this.config, 'DELETE', `/releases/${releaseId}`, { signal }),
389
+
390
+ /** Sync existing release manifests into the backend */
391
+ sync: (body: ReleaseSyncRequest, signal?: AbortSignal): Promise<ReleaseSyncResponse> =>
392
+ apiRequest(this.config, 'POST', '/releases/sync', { body, signal }),
393
+ };
394
+
395
+ // ─── Admin ────────────────────────────────────────────────────────────────
396
+
397
+ readonly admin = {
398
+ /** Get service info and quota limits */
399
+ serviceDetail: (signal?: AbortSignal): Promise<ServiceDetailResponse> =>
400
+ apiRequest(this.config, 'GET', '/admin/service', { signal }),
401
+
402
+ /** Update custom service-wide limits */
403
+ updateService: (body: AdminServiceUpdateRequest, signal?: AbortSignal): Promise<AdminServiceUpdateResponse> =>
404
+ apiRequest(this.config, 'PATCH', '/admin/service', { body, signal }),
405
+
406
+ /** Get real-time storage usage for the service */
407
+ usage: (signal?: AbortSignal): Promise<ServiceUsageResponse> =>
408
+ apiRequest(this.config, 'GET', '/admin/service/usage', { signal }),
409
+
410
+ /** List all uploads (pending/completed/failed) for this service */
411
+ listUploads: (query?: { status?: UploadStatus; cursor?: string; limit?: number }, signal?: AbortSignal): Promise<UploadsListResponse> =>
412
+ apiRequest(this.config, 'GET', '/files', { query, signal }),
413
+
414
+ /** Get a single upload for this service */
415
+ getUpload: (id: string, signal?: AbortSignal): Promise<UploadRecordDetailResponse> =>
416
+ apiRequest(this.config, 'GET', `/files/${id}`, { signal }),
417
+
418
+ /** Get a time-limited download URL for an upload owned by this service */
419
+ downloadUploadUrl: (id: string, signal?: AbortSignal): Promise<FileDownloadUrlResponse> =>
420
+ apiRequest(this.config, 'GET', `/files/${id}/download-url`, { signal }),
421
+
422
+ /** Permanently delete an upload owned by this service */
423
+ deleteUpload: (id: string, signal?: AbortSignal): Promise<FileDeleteResponse> =>
424
+ apiRequest(this.config, 'DELETE', `/files/${id}`, { signal }),
425
+
426
+ /** List audit log events for this service */
427
+ auditLog: (query?: AuditLogListQuery, signal?: AbortSignal): Promise<AuditLogListResponse> =>
428
+ apiRequest(this.config, 'GET', '/admin/audit-log', { query, signal }),
429
+
430
+ /** List Appwrite users together with service-specific metadata */
431
+ listUsers: (
432
+ query?: { search?: string; offset?: number; limit?: number },
433
+ signal?: AbortSignal
434
+ ): Promise<AdminUserListResponse> => apiRequest(this.config, 'GET', '/admin/users', { query, signal }),
435
+
436
+ /** Update Appwrite user properties and service-specific quota/role */
437
+ updateUser: (
438
+ userId: string,
439
+ body: AdminUserUpdateRequest,
440
+ signal?: AbortSignal
441
+ ): Promise<AdminUserUpdateResponse> =>
442
+ apiRequest(this.config, 'PATCH', `/admin/users/${userId}`, { body, signal }),
443
+
444
+ /** Overwrite a user's password and revoke active sessions */
445
+ resetUserPassword: (
446
+ userId: string,
447
+ body: AdminUserPasswordResetRequest,
448
+ signal?: AbortSignal
449
+ ): Promise<AdminUserPasswordResetResponse> =>
450
+ apiRequest(this.config, 'POST', `/admin/users/${userId}/password`, { body, signal }),
451
+ };
452
+
453
+ // ─── Share Links ──────────────────────────────────────────────────────────────
454
+
455
+ readonly shareLinks = {
456
+ /** Create a public share link for a file */
457
+ create: (fileId: string, body: ShareLinkCreateRequest, signal?: AbortSignal): Promise<ShareLinkCreateResponse> =>
458
+ apiRequest(this.config, 'POST', `/my-files/${fileId}/share-links`, { body, signal }),
459
+
460
+ /** List all share links for a file */
461
+ list: (fileId: string, signal?: AbortSignal): Promise<ShareLinkListResponse> =>
462
+ apiRequest(this.config, 'GET', `/my-files/${fileId}/share-links`, { signal }),
463
+
464
+ /** Update a share link (rename, toggle, change password/expiry) */
465
+ update: (linkId: string, body: ShareLinkUpdateRequest, signal?: AbortSignal): Promise<ShareLinkUpdateResponse> =>
466
+ apiRequest(this.config, 'PATCH', `/share-links/${linkId}`, { body, signal }),
467
+
468
+ /** Permanently delete a share link */
469
+ delete: (linkId: string, signal?: AbortSignal): Promise<ShareLinkDeleteResponse> =>
470
+ apiRequest(this.config, 'DELETE', `/share-links/${linkId}`, { signal }),
471
+ };
472
+ }