@unisource/sdk 0.1.2 → 0.2.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/dist/index.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  import { z } from "zod";
2
- //#region src/index.ts
3
- const FILES_DEFAULT_LIMIT = 25;
4
- const FILES_MAX_LIMIT = 100;
5
- const nonEmptyStringSchema = z.string().trim().min(1);
6
- const positiveIntegerSchema = z.number().int().positive();
2
+ //#region src/primitives.ts
3
+ const nonEmptyString = z.string().trim().min(1);
4
+ const positiveInt = z.number().int().positive();
5
+ const unixTimestamp = z.number().int().nonnegative();
7
6
  const uploadDestinationSchema = z.enum(["r2", "appwrite"]);
8
7
  const uploadStatusSchema = z.enum([
9
8
  "pending",
@@ -11,84 +10,306 @@ const uploadStatusSchema = z.enum([
11
10
  "failed"
12
11
  ]);
13
12
  const apiErrorSchema = z.object({
14
- error: nonEmptyStringSchema,
15
- message: nonEmptyStringSchema
13
+ error: nonEmptyString,
14
+ message: nonEmptyString
16
15
  });
16
+ //#endregion
17
+ //#region src/uploads.ts
17
18
  const uploadR2InitRequestSchema = z.object({
18
- filename: nonEmptyStringSchema,
19
- size: positiveIntegerSchema,
20
- mime_type: nonEmptyStringSchema,
21
- bucket: nonEmptyStringSchema.optional()
19
+ filename: nonEmptyString,
20
+ size: positiveInt,
21
+ mime_type: nonEmptyString
22
22
  });
23
23
  const uploadR2InitResponseSchema = z.object({
24
- upload_id: nonEmptyStringSchema,
24
+ upload_id: nonEmptyString,
25
25
  destination: z.literal("r2"),
26
26
  presigned_url: z.string().url(),
27
- storage_key: nonEmptyStringSchema,
28
- bucket: nonEmptyStringSchema,
29
- expires_at: positiveIntegerSchema
27
+ storage_key: nonEmptyString,
28
+ bucket: nonEmptyString,
29
+ expires_at: positiveInt
30
30
  });
31
31
  const uploadAppwriteInitRequestSchema = z.object({
32
- filename: nonEmptyStringSchema,
33
- size: positiveIntegerSchema,
34
- mime_type: nonEmptyStringSchema
32
+ filename: nonEmptyString,
33
+ size: positiveInt,
34
+ mime_type: nonEmptyString
35
35
  });
36
36
  const uploadAppwriteInitResponseSchema = z.object({
37
- upload_id: nonEmptyStringSchema,
37
+ upload_id: nonEmptyString,
38
38
  destination: z.literal("appwrite"),
39
39
  appwrite_endpoint: z.string().url(),
40
- appwrite_project_id: nonEmptyStringSchema,
41
- appwrite_bucket_id: nonEmptyStringSchema,
42
- file_id: nonEmptyStringSchema,
43
- expires_at: positiveIntegerSchema
40
+ appwrite_project_id: nonEmptyString,
41
+ appwrite_bucket_id: nonEmptyString,
42
+ file_id: nonEmptyString,
43
+ expires_at: positiveInt
44
44
  });
45
- const uploadLifecycleRequestSchema = z.object({ upload_id: nonEmptyStringSchema });
45
+ const uploadLifecycleRequestSchema = z.object({ upload_id: nonEmptyString });
46
46
  const uploadCompleteResponseSchema = z.object({
47
47
  success: z.literal(true),
48
- upload_id: nonEmptyStringSchema,
48
+ upload_id: nonEmptyString,
49
49
  status: z.literal("completed")
50
50
  });
51
51
  const uploadFailResponseSchema = z.object({
52
52
  success: z.literal(true),
53
- upload_id: nonEmptyStringSchema,
53
+ upload_id: nonEmptyString,
54
54
  status: z.literal("failed")
55
55
  });
56
- const fileRecordSchema = z.object({
57
- id: nonEmptyStringSchema,
58
- filename: nonEmptyStringSchema,
59
- size: positiveIntegerSchema,
60
- mime_type: nonEmptyStringSchema,
56
+ const FILES_DEFAULT_LIMIT = 25;
57
+ const FILES_MAX_LIMIT = 100;
58
+ /** Raw upload record — returned by admin `/files` endpoints. */
59
+ const uploadRecordSchema = z.object({
60
+ id: nonEmptyString,
61
+ service_id: nonEmptyString,
62
+ user_id: nonEmptyString.nullable(),
63
+ filename: nonEmptyString,
64
+ size: positiveInt,
65
+ mime_type: nonEmptyString,
61
66
  destination: uploadDestinationSchema,
62
- storage_key: nonEmptyStringSchema,
63
- bucket: nonEmptyStringSchema,
64
- status: uploadStatusSchema,
65
- expires_at: positiveIntegerSchema,
66
- created_at: positiveIntegerSchema,
67
- updated_at: positiveIntegerSchema
68
- });
69
- const filesListQuerySchema = z.object({
70
- limit: z.number().int().min(1).max(100).optional(),
71
- cursor: nonEmptyStringSchema.optional(),
72
- destination: uploadDestinationSchema.optional(),
73
- status: uploadStatusSchema.optional()
74
- });
75
- const filesListResponseSchema = z.object({
67
+ status: z.enum([
68
+ "pending",
69
+ "completed",
70
+ "failed"
71
+ ]),
72
+ expires_at: positiveInt,
73
+ created_at: positiveInt,
74
+ updated_at: positiveInt
75
+ });
76
+ const uploadsListResponseSchema = z.object({
77
+ items: z.array(uploadRecordSchema),
78
+ next_cursor: z.string().nullable(),
79
+ limit: positiveInt
80
+ });
81
+ //#endregion
82
+ //#region src/fileRecords.ts
83
+ /**
84
+ * A confirmed file record owned by a user.
85
+ * Internal fields (storage_key, bucket) are intentionally excluded from the public API.
86
+ */
87
+ const fileRecordSchema = z.object({
88
+ id: nonEmptyString,
89
+ service_id: nonEmptyString,
90
+ user_id: nonEmptyString,
91
+ folder_id: nonEmptyString.nullable(),
92
+ upload_id: nonEmptyString.nullable(),
93
+ filename: nonEmptyString,
94
+ size: positiveInt,
95
+ mime_type: nonEmptyString,
96
+ storage_destination: uploadDestinationSchema,
97
+ is_trashed: z.boolean(),
98
+ trashed_at: positiveInt.nullable(),
99
+ created_at: positiveInt,
100
+ updated_at: positiveInt
101
+ });
102
+ const fileRecordsListQuerySchema = z.object({
103
+ folder_id: nonEmptyString.nullable().optional(),
104
+ is_trashed: z.boolean().optional(),
105
+ cursor: nonEmptyString.optional(),
106
+ limit: z.number().int().min(1).max(100).optional()
107
+ });
108
+ const fileRecordsListResponseSchema = z.object({
76
109
  items: z.array(fileRecordSchema),
77
110
  next_cursor: z.string().nullable(),
78
- limit: z.number().int().min(1).max(100)
111
+ limit: positiveInt
79
112
  });
80
- const fileDetailsResponseSchema = z.object({ file: fileRecordSchema });
113
+ const fileRecordDetailResponseSchema = z.object({ file: fileRecordSchema });
114
+ const fileMoveRequestSchema = z.object({ folder_id: nonEmptyString.nullable().optional() });
81
115
  const fileDownloadUrlResponseSchema = z.object({
82
- upload_id: nonEmptyStringSchema,
116
+ upload_id: nonEmptyString,
83
117
  destination: uploadDestinationSchema,
84
118
  download_url: z.string().url(),
85
- expires_at: positiveIntegerSchema
119
+ expires_at: positiveInt
86
120
  });
87
121
  const fileDeleteResponseSchema = z.object({
88
122
  success: z.literal(true),
89
- upload_id: nonEmptyStringSchema,
90
- destination: uploadDestinationSchema,
91
- storage_not_found: z.boolean()
123
+ id: nonEmptyString,
124
+ permanent: z.boolean()
125
+ });
126
+ const fileRestoreResponseSchema = z.object({
127
+ success: z.literal(true),
128
+ id: nonEmptyString
129
+ });
130
+ //#endregion
131
+ //#region src/folders.ts
132
+ const folderSchema = z.object({
133
+ id: nonEmptyString,
134
+ service_id: nonEmptyString,
135
+ user_id: nonEmptyString,
136
+ parent_id: nonEmptyString.nullable(),
137
+ name: nonEmptyString,
138
+ color_tag: z.string().nullable(),
139
+ is_trashed: z.boolean(),
140
+ trashed_at: positiveInt.nullable(),
141
+ created_at: positiveInt,
142
+ updated_at: positiveInt
92
143
  });
144
+ const folderListQuerySchema = z.object({
145
+ parent_id: nonEmptyString.nullable().optional(),
146
+ is_trashed: z.boolean().optional(),
147
+ cursor: nonEmptyString.optional(),
148
+ limit: z.number().int().min(1).max(100).optional()
149
+ });
150
+ const folderListResponseSchema = z.object({
151
+ items: z.array(folderSchema),
152
+ next_cursor: z.string().nullable(),
153
+ limit: positiveInt
154
+ });
155
+ const folderCreateRequestSchema = z.object({
156
+ name: nonEmptyString,
157
+ parent_id: nonEmptyString.optional(),
158
+ color_tag: z.string().optional()
159
+ });
160
+ const folderCreateResponseSchema = z.object({ folder: folderSchema });
161
+ const folderUpdateRequestSchema = z.object({
162
+ name: nonEmptyString.optional(),
163
+ color_tag: z.string().nullable().optional()
164
+ }).refine((v) => v.name !== void 0 || v.color_tag !== void 0, { message: "At least one of name or color_tag must be provided" });
165
+ const folderUpdateResponseSchema = z.object({ folder: folderSchema });
166
+ const folderDeleteResponseSchema = z.object({
167
+ success: z.literal(true),
168
+ id: nonEmptyString,
169
+ permanent: z.boolean(),
170
+ folders_deleted: z.number().int().nonnegative().optional()
171
+ });
172
+ const folderRestoreResponseSchema = z.object({
173
+ success: z.literal(true),
174
+ id: nonEmptyString
175
+ });
176
+ //#endregion
177
+ //#region src/services.ts
178
+ /** Public service info returned to admins. Never exposes secrets. */
179
+ const serviceSchema = z.object({
180
+ id: nonEmptyString,
181
+ name: nonEmptyString,
182
+ max_storage_bytes: positiveInt,
183
+ current_used_bytes: z.number().int().nonnegative(),
184
+ max_file_size_bytes: positiveInt,
185
+ created_at: unixTimestamp
186
+ });
187
+ const serviceDetailResponseSchema = z.object({ service: serviceSchema });
188
+ const serviceUsageResponseSchema = z.object({
189
+ service_id: nonEmptyString,
190
+ max_storage_bytes: positiveInt,
191
+ current_used_bytes: z.number().int().nonnegative(),
192
+ used_percent: z.number().min(0).max(100)
193
+ });
194
+ const auditEventActionSchema = z.enum([
195
+ "upload_completed",
196
+ "file_deleted",
197
+ "folder_deleted",
198
+ "quota_exceeded"
199
+ ]);
200
+ const auditEventSchema = z.object({
201
+ id: nonEmptyString,
202
+ service_id: nonEmptyString,
203
+ user_id: nonEmptyString,
204
+ action: auditEventActionSchema,
205
+ resource_type: z.enum([
206
+ "file",
207
+ "folder",
208
+ "service"
209
+ ]),
210
+ resource_id: nonEmptyString,
211
+ metadata: z.record(z.string(), z.unknown()).nullable(),
212
+ ip_address: z.string().nullable(),
213
+ created_at: unixTimestamp
214
+ });
215
+ const auditLogListQuerySchema = z.object({
216
+ user_id: nonEmptyString.optional(),
217
+ action: auditEventActionSchema.optional(),
218
+ resource_type: z.enum([
219
+ "file",
220
+ "folder",
221
+ "service"
222
+ ]).optional(),
223
+ cursor: nonEmptyString.optional(),
224
+ limit: z.number().int().min(1).max(200).optional()
225
+ });
226
+ const auditLogListResponseSchema = z.object({
227
+ items: z.array(auditEventSchema),
228
+ next_cursor: z.string().nullable(),
229
+ limit: positiveInt
230
+ });
231
+ //#endregion
232
+ //#region src/client.ts
233
+ var UnisourceError = class extends Error {
234
+ constructor(message, status, body) {
235
+ super(message);
236
+ this.status = status;
237
+ this.body = body;
238
+ this.name = "UnisourceError";
239
+ }
240
+ };
241
+ var UnisourceNetworkError = class extends Error {
242
+ constructor(message, cause) {
243
+ super(message);
244
+ this.cause = cause;
245
+ this.name = "UnisourceNetworkError";
246
+ }
247
+ };
248
+ async function apiRequest(config, method, path, options = {}) {
249
+ const token = await config.getToken();
250
+ const url = new URL(path, config.baseUrl);
251
+ if (options.query) {
252
+ for (const [key, value] of Object.entries(options.query)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
253
+ }
254
+ const headers = { "X-Service-ID": config.serviceId };
255
+ if (token) headers["Authorization"] = `Bearer ${token}`;
256
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
257
+ let response;
258
+ try {
259
+ response = await fetch(url.toString(), {
260
+ method,
261
+ headers,
262
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
263
+ });
264
+ } catch (err) {
265
+ throw new UnisourceNetworkError("Network request failed", err);
266
+ }
267
+ if (!response.ok) {
268
+ let body;
269
+ try {
270
+ body = await response.json();
271
+ } catch {
272
+ body = {
273
+ error: "Unknown",
274
+ message: response.statusText
275
+ };
276
+ }
277
+ throw new UnisourceError(body.message, response.status, body);
278
+ }
279
+ return response.json();
280
+ }
281
+ var UnisourceClient = class {
282
+ config;
283
+ constructor(config) {
284
+ this.config = config;
285
+ }
286
+ upload = {
287
+ r2Init: (body) => apiRequest(this.config, "POST", "/upload/r2/init", { body }),
288
+ appwriteInit: (body) => apiRequest(this.config, "POST", "/upload/appwrite/init", { body }),
289
+ complete: (body) => apiRequest(this.config, "POST", "/upload/complete", { body }),
290
+ fail: (body) => apiRequest(this.config, "POST", "/upload/fail", { body })
291
+ };
292
+ myFiles = {
293
+ list: (query) => apiRequest(this.config, "GET", "/my-files", { query }),
294
+ get: (id) => apiRequest(this.config, "GET", `/my-files/${id}`),
295
+ downloadUrl: (id) => apiRequest(this.config, "GET", `/my-files/${id}/download-url`),
296
+ move: (id, body) => apiRequest(this.config, "PATCH", `/my-files/${id}/move`, { body }),
297
+ delete: (id, query) => apiRequest(this.config, "DELETE", `/my-files/${id}`, { query }),
298
+ restore: (id) => apiRequest(this.config, "POST", `/my-files/${id}/restore`)
299
+ };
300
+ folders = {
301
+ list: (query) => apiRequest(this.config, "GET", "/folders", { query }),
302
+ create: (body) => apiRequest(this.config, "POST", "/folders", { body }),
303
+ update: (id, body) => apiRequest(this.config, "PATCH", `/folders/${id}`, { body }),
304
+ delete: (id, query) => apiRequest(this.config, "DELETE", `/folders/${id}`, { query }),
305
+ restore: (id) => apiRequest(this.config, "POST", `/folders/${id}/restore`)
306
+ };
307
+ admin = {
308
+ serviceDetail: () => apiRequest(this.config, "GET", "/admin/service"),
309
+ usage: () => apiRequest(this.config, "GET", "/admin/service/usage"),
310
+ listUploads: (query) => apiRequest(this.config, "GET", "/files", { query }),
311
+ auditLog: (query) => apiRequest(this.config, "GET", "/admin/audit-log", { query })
312
+ };
313
+ };
93
314
  //#endregion
94
- export { FILES_DEFAULT_LIMIT, FILES_MAX_LIMIT, apiErrorSchema, fileDeleteResponseSchema, fileDetailsResponseSchema, fileDownloadUrlResponseSchema, fileRecordSchema, filesListQuerySchema, filesListResponseSchema, uploadAppwriteInitRequestSchema, uploadAppwriteInitResponseSchema, uploadCompleteResponseSchema, uploadDestinationSchema, uploadFailResponseSchema, uploadLifecycleRequestSchema, uploadR2InitRequestSchema, uploadR2InitResponseSchema, uploadStatusSchema };
315
+ export { FILES_DEFAULT_LIMIT, FILES_MAX_LIMIT, UnisourceClient, UnisourceError, UnisourceNetworkError, apiErrorSchema, auditEventActionSchema, auditEventSchema, auditLogListQuerySchema, auditLogListResponseSchema, fileDeleteResponseSchema, fileDownloadUrlResponseSchema, fileMoveRequestSchema, fileRecordDetailResponseSchema, fileRecordSchema, fileRecordsListQuerySchema, fileRecordsListResponseSchema, fileRestoreResponseSchema, folderCreateRequestSchema, folderCreateResponseSchema, folderDeleteResponseSchema, folderListQuerySchema, folderListResponseSchema, folderRestoreResponseSchema, folderSchema, folderUpdateRequestSchema, folderUpdateResponseSchema, nonEmptyString, positiveInt, serviceDetailResponseSchema, serviceSchema, serviceUsageResponseSchema, unixTimestamp, uploadAppwriteInitRequestSchema, uploadAppwriteInitResponseSchema, uploadCompleteResponseSchema, uploadDestinationSchema, uploadFailResponseSchema, uploadLifecycleRequestSchema, uploadR2InitRequestSchema, uploadR2InitResponseSchema, uploadRecordSchema, uploadStatusSchema, uploadsListResponseSchema };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@unisource/sdk",
3
3
  "private": false,
4
4
  "type": "module",
5
- "version": "0.1.2",
5
+ "version": "0.2.0",
6
6
  "description": "Wspolne kontrakty danych dla backendu i frontendu UniSource.",
7
7
  "license": "MIT",
8
8
  "publishConfig": {
@@ -30,7 +30,7 @@
30
30
  "scripts": {
31
31
  "build": "tsdown && node -e \"const fs=require('node:fs'); if (fs.existsSync('dist/index.d.mts')) fs.copyFileSync('dist/index.d.mts','dist/index.d.ts');\"",
32
32
  "dev": "tsdown --watch",
33
- "test": "vitest",
33
+ "test": "pnpm run build && vitest",
34
34
  "typecheck": "tsc --noEmit"
35
35
  }
36
36
  }
package/src/client.ts ADDED
@@ -0,0 +1,233 @@
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
+ }