@nurama/sdk 0.0.0-stage → 1.4.1

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.
Files changed (225) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1080 -2
  4. package/dist/BotClient.d.ts +66 -0
  5. package/dist/BotClient.d.ts.map +1 -0
  6. package/dist/BotClient.js +68 -0
  7. package/dist/BotClient.js.map +1 -0
  8. package/dist/NuramaClient.d.ts +480 -0
  9. package/dist/NuramaClient.d.ts.map +1 -0
  10. package/dist/NuramaClient.js +902 -0
  11. package/dist/NuramaClient.js.map +1 -0
  12. package/dist/browser/nurama-bot-sdk.js +12051 -0
  13. package/dist/browser/nurama-bot-sdk.min.js +1 -0
  14. package/dist/browser/nurama-sdk.js +12003 -0
  15. package/dist/browser/nurama-sdk.min.js +1 -0
  16. package/dist/routes/ai.d.ts +280 -0
  17. package/dist/routes/ai.d.ts.map +1 -0
  18. package/dist/routes/ai.js +173 -0
  19. package/dist/routes/ai.js.map +1 -0
  20. package/dist/routes/asset.d.ts +493 -0
  21. package/dist/routes/asset.d.ts.map +1 -0
  22. package/dist/routes/asset.js +848 -0
  23. package/dist/routes/asset.js.map +1 -0
  24. package/dist/routes/auth.d.ts +218 -0
  25. package/dist/routes/auth.d.ts.map +1 -0
  26. package/dist/routes/auth.js +454 -0
  27. package/dist/routes/auth.js.map +1 -0
  28. package/dist/routes/blogPosts.d.ts +17 -0
  29. package/dist/routes/blogPosts.d.ts.map +1 -0
  30. package/dist/routes/blogPosts.js +29 -0
  31. package/dist/routes/blogPosts.js.map +1 -0
  32. package/dist/routes/board.d.ts +187 -0
  33. package/dist/routes/board.d.ts.map +1 -0
  34. package/dist/routes/board.js +270 -0
  35. package/dist/routes/board.js.map +1 -0
  36. package/dist/routes/bot.d.ts +202 -0
  37. package/dist/routes/bot.d.ts.map +1 -0
  38. package/dist/routes/bot.js +229 -0
  39. package/dist/routes/bot.js.map +1 -0
  40. package/dist/routes/chat.d.ts +842 -0
  41. package/dist/routes/chat.d.ts.map +1 -0
  42. package/dist/routes/chat.js +863 -0
  43. package/dist/routes/chat.js.map +1 -0
  44. package/dist/routes/chatAi.d.ts +51 -0
  45. package/dist/routes/chatAi.d.ts.map +1 -0
  46. package/dist/routes/chatAi.js +109 -0
  47. package/dist/routes/chatAi.js.map +1 -0
  48. package/dist/routes/config.d.ts +11 -0
  49. package/dist/routes/config.d.ts.map +1 -0
  50. package/dist/routes/config.js +24 -0
  51. package/dist/routes/config.js.map +1 -0
  52. package/dist/routes/convo.d.ts +169 -0
  53. package/dist/routes/convo.d.ts.map +1 -0
  54. package/dist/routes/convo.js +284 -0
  55. package/dist/routes/convo.js.map +1 -0
  56. package/dist/routes/credits.d.ts +82 -0
  57. package/dist/routes/credits.d.ts.map +1 -0
  58. package/dist/routes/credits.js +49 -0
  59. package/dist/routes/credits.js.map +1 -0
  60. package/dist/routes/device.d.ts +74 -0
  61. package/dist/routes/device.d.ts.map +1 -0
  62. package/dist/routes/device.js +122 -0
  63. package/dist/routes/device.js.map +1 -0
  64. package/dist/routes/folder.d.ts +75 -0
  65. package/dist/routes/folder.d.ts.map +1 -0
  66. package/dist/routes/folder.js +99 -0
  67. package/dist/routes/folder.js.map +1 -0
  68. package/dist/routes/invite.d.ts +61 -0
  69. package/dist/routes/invite.d.ts.map +1 -0
  70. package/dist/routes/invite.js +86 -0
  71. package/dist/routes/invite.js.map +1 -0
  72. package/dist/routes/joinLink.d.ts +88 -0
  73. package/dist/routes/joinLink.d.ts.map +1 -0
  74. package/dist/routes/joinLink.js +205 -0
  75. package/dist/routes/joinLink.js.map +1 -0
  76. package/dist/routes/membership.d.ts +116 -0
  77. package/dist/routes/membership.d.ts.map +1 -0
  78. package/dist/routes/membership.js +183 -0
  79. package/dist/routes/membership.js.map +1 -0
  80. package/dist/routes/notification.d.ts +103 -0
  81. package/dist/routes/notification.d.ts.map +1 -0
  82. package/dist/routes/notification.js +89 -0
  83. package/dist/routes/notification.js.map +1 -0
  84. package/dist/routes/oauthGrant.d.ts +45 -0
  85. package/dist/routes/oauthGrant.d.ts.map +1 -0
  86. package/dist/routes/oauthGrant.js +32 -0
  87. package/dist/routes/oauthGrant.js.map +1 -0
  88. package/dist/routes/payment.d.ts +56 -0
  89. package/dist/routes/payment.d.ts.map +1 -0
  90. package/dist/routes/payment.js +78 -0
  91. package/dist/routes/payment.js.map +1 -0
  92. package/dist/routes/product.d.ts +43 -0
  93. package/dist/routes/product.d.ts.map +1 -0
  94. package/dist/routes/product.js +53 -0
  95. package/dist/routes/product.js.map +1 -0
  96. package/dist/routes/project.d.ts +821 -0
  97. package/dist/routes/project.d.ts.map +1 -0
  98. package/dist/routes/project.js +1153 -0
  99. package/dist/routes/project.js.map +1 -0
  100. package/dist/routes/public.d.ts +269 -0
  101. package/dist/routes/public.d.ts.map +1 -0
  102. package/dist/routes/public.js +412 -0
  103. package/dist/routes/public.js.map +1 -0
  104. package/dist/routes/scratch.d.ts +70 -0
  105. package/dist/routes/scratch.d.ts.map +1 -0
  106. package/dist/routes/scratch.js +67 -0
  107. package/dist/routes/scratch.js.map +1 -0
  108. package/dist/routes/settings.d.ts +102 -0
  109. package/dist/routes/settings.d.ts.map +1 -0
  110. package/dist/routes/settings.js +94 -0
  111. package/dist/routes/settings.js.map +1 -0
  112. package/dist/routes/shortlink.d.ts +79 -0
  113. package/dist/routes/shortlink.d.ts.map +1 -0
  114. package/dist/routes/shortlink.js +25 -0
  115. package/dist/routes/shortlink.js.map +1 -0
  116. package/dist/routes/socket.d.ts +108 -0
  117. package/dist/routes/socket.d.ts.map +1 -0
  118. package/dist/routes/socket.js +573 -0
  119. package/dist/routes/socket.js.map +1 -0
  120. package/dist/routes/storage.d.ts +44 -0
  121. package/dist/routes/storage.d.ts.map +1 -0
  122. package/dist/routes/storage.js +49 -0
  123. package/dist/routes/storage.js.map +1 -0
  124. package/dist/routes/subscription.d.ts +184 -0
  125. package/dist/routes/subscription.d.ts.map +1 -0
  126. package/dist/routes/subscription.js +219 -0
  127. package/dist/routes/subscription.js.map +1 -0
  128. package/dist/routes/supportChat.d.ts +40 -0
  129. package/dist/routes/supportChat.d.ts.map +1 -0
  130. package/dist/routes/supportChat.js +53 -0
  131. package/dist/routes/supportChat.js.map +1 -0
  132. package/dist/routes/supportTicket.d.ts +89 -0
  133. package/dist/routes/supportTicket.d.ts.map +1 -0
  134. package/dist/routes/supportTicket.js +54 -0
  135. package/dist/routes/supportTicket.js.map +1 -0
  136. package/dist/routes/tag.d.ts +72 -0
  137. package/dist/routes/tag.d.ts.map +1 -0
  138. package/dist/routes/tag.js +81 -0
  139. package/dist/routes/tag.js.map +1 -0
  140. package/dist/routes/task.d.ts +252 -0
  141. package/dist/routes/task.d.ts.map +1 -0
  142. package/dist/routes/task.js +284 -0
  143. package/dist/routes/task.js.map +1 -0
  144. package/dist/routes/taskRelation.d.ts +80 -0
  145. package/dist/routes/taskRelation.d.ts.map +1 -0
  146. package/dist/routes/taskRelation.js +71 -0
  147. package/dist/routes/taskRelation.js.map +1 -0
  148. package/dist/routes/token.d.ts +97 -0
  149. package/dist/routes/token.d.ts.map +1 -0
  150. package/dist/routes/token.js +73 -0
  151. package/dist/routes/token.js.map +1 -0
  152. package/dist/routes/user.d.ts +112 -0
  153. package/dist/routes/user.d.ts.map +1 -0
  154. package/dist/routes/user.js +151 -0
  155. package/dist/routes/user.js.map +1 -0
  156. package/dist/routes/version.d.ts +42 -0
  157. package/dist/routes/version.d.ts.map +1 -0
  158. package/dist/routes/version.js +38 -0
  159. package/dist/routes/version.js.map +1 -0
  160. package/dist/routes/webhook.d.ts +170 -0
  161. package/dist/routes/webhook.d.ts.map +1 -0
  162. package/dist/routes/webhook.js +173 -0
  163. package/dist/routes/webhook.js.map +1 -0
  164. package/dist/routes/workspace.d.ts +120 -0
  165. package/dist/routes/workspace.d.ts.map +1 -0
  166. package/dist/routes/workspace.js +199 -0
  167. package/dist/routes/workspace.js.map +1 -0
  168. package/dist/utils/uploadSessionManager.d.ts +133 -0
  169. package/dist/utils/uploadSessionManager.d.ts.map +1 -0
  170. package/dist/utils/uploadSessionManager.js +321 -0
  171. package/dist/utils/uploadSessionManager.js.map +1 -0
  172. package/dist/utils/urlParams.d.ts +35 -0
  173. package/dist/utils/urlParams.d.ts.map +1 -0
  174. package/dist/utils/urlParams.js +146 -0
  175. package/dist/utils/urlParams.js.map +1 -0
  176. package/dist/version.d.ts +15 -0
  177. package/dist/version.d.ts.map +1 -0
  178. package/dist/version.js +12 -0
  179. package/dist/version.js.map +1 -0
  180. package/package.json +87 -3
  181. package/src/BotClient.ts +113 -0
  182. package/src/NuramaClient.ts +1253 -0
  183. package/src/bot-browser-entry.js +15 -0
  184. package/src/browser-entry.js +20 -0
  185. package/src/routes/ai.ts +378 -0
  186. package/src/routes/asset.ts +1104 -0
  187. package/src/routes/auth.ts +587 -0
  188. package/src/routes/blogPosts.ts +29 -0
  189. package/src/routes/board.ts +403 -0
  190. package/src/routes/bot.ts +356 -0
  191. package/src/routes/chat.ts +1292 -0
  192. package/src/routes/chatAi.ts +125 -0
  193. package/src/routes/config.ts +31 -0
  194. package/src/routes/convo.ts +321 -0
  195. package/src/routes/credits.ts +112 -0
  196. package/src/routes/device.ts +133 -0
  197. package/src/routes/folder.ts +154 -0
  198. package/src/routes/invite.ts +133 -0
  199. package/src/routes/joinLink.ts +233 -0
  200. package/src/routes/membership.ts +237 -0
  201. package/src/routes/notification.ts +166 -0
  202. package/src/routes/oauthGrant.ts +64 -0
  203. package/src/routes/payment.ts +104 -0
  204. package/src/routes/product.ts +67 -0
  205. package/src/routes/project.ts +1528 -0
  206. package/src/routes/public.ts +496 -0
  207. package/src/routes/scratch.ts +94 -0
  208. package/src/routes/settings.ts +152 -0
  209. package/src/routes/shortlink.ts +90 -0
  210. package/src/routes/socket.ts +757 -0
  211. package/src/routes/storage.ts +83 -0
  212. package/src/routes/subscription.ts +307 -0
  213. package/src/routes/supportChat.ts +62 -0
  214. package/src/routes/supportTicket.ts +114 -0
  215. package/src/routes/tag.ts +131 -0
  216. package/src/routes/task.ts +431 -0
  217. package/src/routes/taskRelation.ts +125 -0
  218. package/src/routes/token.ts +152 -0
  219. package/src/routes/user.ts +214 -0
  220. package/src/routes/version.ts +62 -0
  221. package/src/routes/webhook.ts +295 -0
  222. package/src/routes/workspace.ts +223 -0
  223. package/src/utils/uploadSessionManager.ts +407 -0
  224. package/src/utils/urlParams.ts +181 -0
  225. package/src/version.ts +22 -0
@@ -0,0 +1,1104 @@
1
+ export interface DocumentViewUrlResponse {
2
+ /** Signed URL for the document's `media` PDF, served inline. */
3
+ url: string;
4
+ /** Unix seconds. Refetch rather than reusing a URL past this. */
5
+ expires: number;
6
+ /** From `meta.document.pageCount`; 0 when processing has not reported it. */
7
+ pageCount: number;
8
+ /**
9
+ * True when the `media` PDF holds only the first `pageCount` pages of a
10
+ * longer document, because post-processing hit its `maxPages` cap. The PDF
11
+ * itself is internally consistent, so this flag is the only way to know.
12
+ */
13
+ pagesTruncated: boolean;
14
+ /**
15
+ * Which end of the document the `media` PDF kept. `'start'` for everything
16
+ * read front-to-back; `'end'` for logs, whose newest lines are at the bottom.
17
+ * Only meaningful when `pagesTruncated` is true.
18
+ */
19
+ truncatedFrom: 'start' | 'end';
20
+ /** Pages in the source document. Equals `pageCount` unless truncated. */
21
+ originalPageCount: number;
22
+ }
23
+
24
+ import NuramaClient from '../NuramaClient.js';
25
+ import { uploadSessionManager } from '../utils/uploadSessionManager.js';
26
+ import { type Asset, type File, type AssetReferences } from '@nurama/types'; // Import Asset and File types
27
+
28
+ // --- Request Body Interfaces ---
29
+ export interface UpdateAssetData {
30
+ name?: string;
31
+ meta?: Record<string, any>;
32
+ tags?: string[]; // Note: Using string[] for backward compatibility - backend will convert to ObjectId[]
33
+ folderId?: string | null; // Allow setting or unsetting folder
34
+ }
35
+
36
+ export interface TagAssetData {
37
+ tagId: string;
38
+ }
39
+
40
+ export interface UntagAssetData {
41
+ tagId: string;
42
+ }
43
+
44
+ export interface CreateAssetShortLinkData {
45
+ visibility?: 'creator' | 'reviewer';
46
+ }
47
+
48
+ export interface ShortLink {
49
+ id: string;
50
+ code: string;
51
+ resourceId: string;
52
+ resourceType: 'asset' | 'project' | 'workspace' | 'chatMessage' | 'chatSubmission';
53
+ visibility: 'creator' | 'reviewer' | null;
54
+ creatorId: string;
55
+ createdAt: string;
56
+ updatedAt?: string;
57
+ }
58
+
59
+ export interface CreateAssetShortLinkResponse {
60
+ shortLink: ShortLink;
61
+ shortUrl: string;
62
+ }
63
+
64
+ export type PublicAssetLinkMode = 'download' | 'embed' | 'embed-download';
65
+
66
+ export interface PublicAssetLink {
67
+ id: string;
68
+ token: string;
69
+ assetId: string;
70
+ projectId: string;
71
+ creatorId: string;
72
+ expires: string | null;
73
+ status: 'active' | 'disabled' | 'expired';
74
+ /**
75
+ * Capability mode for this link.
76
+ * - 'download' — direct-download link only; embed iframe blocked.
77
+ * - 'embed' — embeddable iframe only; direct download blocked.
78
+ * - 'embed-download' — both endpoints allowed (default; backward-compatible).
79
+ * Enforced server-side at /v1/public-download/{token}/download and /embed-files.
80
+ */
81
+ mode?: PublicAssetLinkMode;
82
+ publicUrl: string;
83
+ isExpired?: boolean;
84
+ createdAt: string;
85
+ updatedAt: string;
86
+ }
87
+
88
+ export interface CompleteMultipartUploadData {
89
+ key: string;
90
+ uploadId: string;
91
+ parts: { ETag: string; PartNumber: number }[];
92
+ assetId: string;
93
+ }
94
+
95
+ // For multipart uploads
96
+ export interface MultipartUploadOptions {
97
+ onProgress?: (progress: { partNumber: number, totalParts: number, percentComplete: number, totalPercentComplete: number }) => void;
98
+ maxRetries?: number;
99
+ abortSignal?: AbortSignal;
100
+
101
+ // Upload session management
102
+ enableProgressPersistence?: boolean; // Enable automatic progress persistence
103
+ projectId?: string; // Project ID for session isolation
104
+ sessionId?: string; // Custom session ID (auto-generated if not provided)
105
+ fileName?: string; // File name for session tracking
106
+ fileSize?: number; // File size for session tracking
107
+ assetId?: string; // Asset ID once available
108
+ }
109
+
110
+ // Internal interface for part-specific progress (no totalPercentComplete)
111
+ export interface PartUploadOptions {
112
+ onPartProgress?: (progress: { partNumber: number, totalParts: number, partPercent: number }) => void;
113
+ maxRetries?: number;
114
+ abortSignal?: AbortSignal;
115
+ }
116
+
117
+ export interface MultipartUploadResult {
118
+ key: string;
119
+ uploadId: string;
120
+ parts: { ETag: string; PartNumber: number }[];
121
+ }
122
+
123
+ // --- Query Parameter Interfaces ---
124
+ export interface GetAssetParams {
125
+ chatVisibility?: 'creator' | 'reviewer';
126
+ chatMessageSort?: {
127
+ id?: 1 | -1;
128
+ };
129
+ chatReplySort?: {
130
+ id?: 1 | -1;
131
+ };
132
+ chatMessageLimit?: number;
133
+ chatReplyLimit?: number;
134
+ }
135
+
136
+ export interface GetAssetPageParams {
137
+ sort?: Record<string, 1 | -1>;
138
+ limit?: number;
139
+ visibility?: 'creator' | 'reviewer' | string; // Allow other roles if applicable
140
+ inFolder?: boolean; // Based on test query params
141
+ // Add other potential query params based on validation if needed
142
+ }
143
+
144
+ // --- Response Interfaces ---
145
+ export interface Chat {
146
+ _id?: string;
147
+ id?: string;
148
+ totalMessages?: number;
149
+ recentMessages?: any[];
150
+ // Add other chat properties as needed
151
+ }
152
+
153
+ export interface AssetWithChats extends Omit<Asset, 'chats'> {
154
+ chats?: {
155
+ creator?: Chat;
156
+ reviewer?: Chat;
157
+ };
158
+ }
159
+
160
+ export type AssetResponse = AssetWithChats; // Enhanced to potentially include chats
161
+ export type FileResponse = File; // Use File type from nurama-types
162
+ export type AssetPageResponse = { page: number }; // Keep local definition for now
163
+ export type RepairAssetsResponse = any[]; // Keep as any for now
164
+ export type DownloadAssetsResponse = any[]; // Keep as any for now
165
+
166
+ /**
167
+ * Defines asset-related methods for the NuramaClient.
168
+ * @param {NuramaClient} client - The NuramaClient instance.
169
+ * @returns {object} An object containing the asset-related methods.
170
+ */
171
+ export default function createAssetMethods(client: NuramaClient) {
172
+ /**
173
+ * Uploads a single part of a file
174
+ * @private
175
+ * @param {string} signedUrl - The signed URL for uploading this part
176
+ * @param {number} partNumber - The part number (1-based index)
177
+ * @param {number} totalParts - Total number of parts
178
+ * @param {Blob|Buffer|ArrayBuffer} data - The data chunk to upload
179
+ * @param {PartUploadOptions} options - Upload options
180
+ * @param {number} attempt - Current attempt number
181
+ * @returns {Promise<{ETag: string, PartNumber: number}>} The completed part information
182
+ */
183
+ async function uploadPart(
184
+ signedUrl: string,
185
+ partNumber: number,
186
+ totalParts: number,
187
+ data: Blob | Buffer | ArrayBuffer,
188
+ options: PartUploadOptions = {},
189
+ attempt: number = 1
190
+ ): Promise<{ ETag: string; PartNumber: number }> {
191
+ const { onPartProgress, maxRetries = 3, abortSignal } = options;
192
+
193
+ try {
194
+ // Report start of part upload
195
+ if (onPartProgress) {
196
+ onPartProgress({
197
+ partNumber,
198
+ totalParts,
199
+ partPercent: 0
200
+ });
201
+ }
202
+
203
+ // Check for abort signal
204
+ if (abortSignal?.aborted) {
205
+ throw new Error('Upload aborted by user');
206
+ }
207
+
208
+ // Create headers
209
+ const headers: Record<string, string> = {
210
+ 'Content-Type': 'application/octet-stream',
211
+ };
212
+
213
+ // Set Content-Length only in Node.js (Buffer). In browsers, Content-Length
214
+ // is a forbidden header that gets silently stripped — the browser calculates
215
+ // it automatically from the body.
216
+ if (typeof Buffer !== 'undefined' && data instanceof Buffer) {
217
+ headers['Content-Length'] = data.length.toString();
218
+ }
219
+
220
+ // Upload the part
221
+ // Note: Buffer extends Uint8Array, which is a valid BodyInit type
222
+ const response = await fetch(signedUrl, {
223
+ method: 'PUT',
224
+ headers,
225
+ body: data as BodyInit,
226
+ signal: abortSignal
227
+ });
228
+
229
+ if (!response.ok) {
230
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
231
+ }
232
+
233
+ // Get ETag from response headers
234
+ const eTag = response.headers.get('ETag')?.replace(/['"]/g, '') || '';
235
+ if (!eTag) {
236
+ throw new Error('Server did not return an ETag');
237
+ }
238
+
239
+ // Report completion of part upload
240
+ if (onPartProgress) {
241
+ onPartProgress({
242
+ partNumber,
243
+ totalParts,
244
+ partPercent: 100
245
+ });
246
+ }
247
+
248
+ return {
249
+ ETag: eTag,
250
+ PartNumber: partNumber
251
+ };
252
+ } catch (error) {
253
+ // Handle retries
254
+ if (attempt < maxRetries) {
255
+ console.warn(`Part ${partNumber} upload failed (attempt ${attempt}/${maxRetries}). Retrying...`);
256
+ // Exponential backoff with jitter
257
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1) * (0.9 + Math.random() * 0.2), 10000);
258
+ await new Promise(resolve => setTimeout(resolve, delay));
259
+ return uploadPart(signedUrl, partNumber, totalParts, data, options, attempt + 1);
260
+ }
261
+
262
+ throw error;
263
+ }
264
+ }
265
+
266
+ return {
267
+ /**
268
+ * Retrieves a specific asset by its ID with optional chat data.
269
+ * @param {string} assetId - The ID of the asset.
270
+ * @param {GetAssetParams} [params] - Optional parameters for chat data inclusion.
271
+ * @returns {Promise<AssetResponse>} The asset object with optional chat data.
272
+ */
273
+ async getAsset(assetId: string, params?: GetAssetParams): Promise<AssetResponse> {
274
+ if (!assetId) throw new Error('assetId is required.');
275
+ return client._request<AssetResponse>({
276
+ method: 'GET',
277
+ endpoint: `/v1/assets/${assetId}`,
278
+ params,
279
+ sendJWT: true,
280
+ });
281
+ },
282
+
283
+ /**
284
+ * Lists every location an asset is referenced — its primary file system and
285
+ * each secondary reference (reviewer, submission, public), grouped and counted.
286
+ *
287
+ * Renaming an asset retitles it at every one of these locations, and deleting
288
+ * its last primary reference removes them all — so this is what the rename and
289
+ * delete confirmations show the user before either happens.
290
+ *
291
+ * @param {string} assetId - The ID of the asset.
292
+ * @returns {Promise<AssetReferences>} The asset's references.
293
+ */
294
+ async getAssetReferences(assetId: string): Promise<AssetReferences> {
295
+ if (!assetId) throw new Error('assetId is required.');
296
+ return client._request<AssetReferences>({
297
+ method: 'GET',
298
+ endpoint: `/v1/assets/${assetId}/references`,
299
+ sendJWT: true,
300
+ });
301
+ },
302
+
303
+ /**
304
+ * Updates an asset.
305
+ * @param {string} assetId - The ID of the asset to update.
306
+ * @param {UpdateAssetData} updateData - Data to update (e.g., name, meta, tags, folderId).
307
+ * @returns {Promise<AssetResponse>} The updated asset object.
308
+ */
309
+ async updateAsset(assetId: string, updateData: UpdateAssetData): Promise<AssetResponse> {
310
+ if (!assetId) throw new Error('assetId is required.');
311
+ return client._request<AssetResponse>({
312
+ method: 'PUT',
313
+ endpoint: `/v1/assets/${assetId}`,
314
+ body: updateData,
315
+ sendJWT: true,
316
+ });
317
+ },
318
+
319
+ /**
320
+ * Deletes an asset (marks for deletion).
321
+ * @param {string} assetId - The ID of the asset to delete.
322
+ * @returns {Promise<void>}
323
+ */
324
+ async deleteAsset(assetId: string): Promise<AssetResponse> {
325
+ if (!assetId) throw new Error('assetId is required.');
326
+ return client._request({
327
+ method: 'DELETE',
328
+ endpoint: `/v1/assets/${assetId}`,
329
+ sendJWT: true,
330
+ });
331
+ },
332
+
333
+ /**
334
+ * Retrieves a specific file from an asset.
335
+ * @param {string} assetId - The ID of the asset.
336
+ * @param {string} fileId - The ID of the file.
337
+ * @returns {Promise<FileResponse>} The file object.
338
+ */
339
+ async getFile(assetId: string, fileId: string): Promise<FileResponse> {
340
+ if (!assetId) throw new Error('assetId is required.');
341
+ if (!fileId) throw new Error('fileId is required.');
342
+ return client._request<FileResponse>({
343
+ method: 'GET',
344
+ endpoint: `/v1/assets/${assetId}/file/${fileId}`,
345
+ sendJWT: true,
346
+ });
347
+ },
348
+
349
+ /**
350
+ * Retrieves files of a specific function type from an asset.
351
+ * @param {string} assetId - The ID of the asset.
352
+ * @param {string} functionType - The function type of the files (e.g., 'thumbnail', 'original').
353
+ * @returns {Promise<FileResponse[]>} An array of file objects.
354
+ */
355
+ async getFilesByFunctionType(assetId: string, functionType: string): Promise<FileResponse[]> {
356
+ if (!assetId) throw new Error('assetId is required.');
357
+ if (!functionType) throw new Error('functionType is required.');
358
+ return client._request<FileResponse[]>({
359
+ method: 'GET',
360
+ endpoint: `/v1/assets/${assetId}/function-type/${functionType}`,
361
+ sendJWT: true,
362
+ });
363
+ },
364
+
365
+ /**
366
+ * Complete a multipart upload initiated by `createAssets`.
367
+ *
368
+ * Matches the route (`POST /v1/assets/complete-upload`) and mirrors
369
+ * `nuramaClient.scratch.completeUpload`, so moving between the asset
370
+ * and scratch namespaces uses the same verb.
371
+ *
372
+ * @param {CompleteMultipartUploadData} uploadData - Data including key, uploadId, parts, and assetId.
373
+ * @returns {Promise<any>} S3 completion response.
374
+ */
375
+ async completeUpload(uploadData: CompleteMultipartUploadData): Promise<any> {
376
+ if (!uploadData || !uploadData.key || !uploadData.uploadId || !uploadData.parts || !uploadData.assetId) {
377
+ throw new Error('key, uploadId, parts, and assetId are required for completeUpload.');
378
+ }
379
+ return client._request({
380
+ method: 'POST',
381
+ endpoint: '/v1/assets/complete-upload',
382
+ body: uploadData,
383
+ sendJWT: true,
384
+ });
385
+ },
386
+
387
+ /**
388
+ * Uploads a file using multipart upload with the provided signed URLs
389
+ * @param {File|Blob|Buffer|string} file - The file to upload (File/Blob in browser, Buffer/string path in Node.js)
390
+ * @param {string[]} signedUrls - Array of signed URLs for each part
391
+ * @param {string} key - The S3 key for the upload
392
+ * @param {string} uploadId - The S3 uploadId for the multipart upload
393
+ * @param {MultipartUploadOptions} options - Upload options
394
+ * @returns {Promise<MultipartUploadResult>} The completed upload data
395
+ */
396
+ async multipartUpload(
397
+ file: File | Blob | Buffer | string,
398
+ signedUrls: string[],
399
+ key: string,
400
+ uploadId: string,
401
+ options: MultipartUploadOptions = {}
402
+ ): Promise<MultipartUploadResult> {
403
+ if (!file) throw new Error('file is required');
404
+ if (!signedUrls || !signedUrls.length) throw new Error('signedUrls array is required and cannot be empty');
405
+ if (!key) throw new Error('key is required');
406
+ if (!uploadId) throw new Error('uploadId is required');
407
+
408
+ const {
409
+ onProgress,
410
+ abortSignal,
411
+ enableProgressPersistence,
412
+ projectId,
413
+ sessionId: providedSessionId,
414
+ fileName,
415
+ fileSize: providedFileSize,
416
+ assetId
417
+ } = options;
418
+ const totalParts = signedUrls.length;
419
+ const completedParts: { ETag: string; PartNumber: number }[] = [];
420
+
421
+ // Track progress of each part
422
+ const partProgress: number[] = new Array(totalParts).fill(0);
423
+
424
+ const calculateTotalProgress = () => {
425
+ const totalPercent = partProgress.reduce((sum, percent) => sum + percent, 0) / totalParts;
426
+ return Math.floor(totalPercent);
427
+ };
428
+
429
+ // Initialize upload session if enabled
430
+ let sessionData: any = null;
431
+ if (enableProgressPersistence && projectId) {
432
+ const actualSessionId = providedSessionId || `upload_${uploadId}_${Date.now()}`;
433
+ const actualFileName = fileName || (typeof file === 'string' ? file.split('/').pop() || 'unknown' : 'unknown');
434
+ let actualFileSize = providedFileSize;
435
+
436
+ // Get file size if not provided
437
+ if (!actualFileSize) {
438
+ if ((typeof File !== 'undefined' && file instanceof File) ||
439
+ (typeof Blob !== 'undefined' && file instanceof Blob)) {
440
+ actualFileSize = (file as Blob).size;
441
+ } else if (typeof Buffer !== 'undefined' && file instanceof Buffer) {
442
+ actualFileSize = file.length;
443
+ }
444
+ }
445
+
446
+ try {
447
+ sessionData = uploadSessionManager.createSession(projectId, actualSessionId, {
448
+ uploadId,
449
+ key,
450
+ fileName: actualFileName,
451
+ fileSize: actualFileSize || 0,
452
+ totalParts,
453
+ assetId
454
+ });
455
+ } catch (error) {
456
+ console.warn('[SDK] Failed to create upload session:', error);
457
+ }
458
+ }
459
+
460
+ try {
461
+ // Get file data according to environment
462
+ let fileSize: number;
463
+ let getChunk: (start: number, end: number) => Promise<Blob | Buffer | ArrayBuffer>;
464
+
465
+ // Browser environment (File/Blob)
466
+ if ((typeof File !== 'undefined' && file instanceof File) ||
467
+ (typeof Blob !== 'undefined' && file instanceof Blob)) {
468
+ const blob = file as Blob;
469
+ fileSize = blob.size;
470
+
471
+ getChunk = async (start: number, end: number) => {
472
+ // Read slice into ArrayBuffer to materialize the data before upload.
473
+ // Passing a Blob reference directly to fetch() can silently send
474
+ // empty bodies for large files when the browser loses the file handle.
475
+ const slice = blob.slice(start, end);
476
+ let buffer: ArrayBuffer;
477
+ if (typeof slice.arrayBuffer === 'function') {
478
+ buffer = await slice.arrayBuffer();
479
+ } else {
480
+ // React Native Blob polyfill lacks arrayBuffer() — use FileReader fallback
481
+ buffer = await new Promise<ArrayBuffer>((resolve, reject) => {
482
+ const reader = new FileReader();
483
+ reader.onload = () => resolve(reader.result as ArrayBuffer);
484
+ reader.onerror = () => reject(new Error('Failed to read blob chunk'));
485
+ reader.readAsArrayBuffer(slice);
486
+ });
487
+ }
488
+ if (buffer.byteLength === 0 && end > start) {
489
+ throw new Error(`Failed to read file bytes ${start}-${end}: got 0 bytes (file may have been modified or removed)`);
490
+ }
491
+ return buffer;
492
+ };
493
+ }
494
+ // Node.js environment with Buffer
495
+ else if (typeof Buffer !== 'undefined' && file instanceof Buffer) {
496
+ const buffer = file as Buffer;
497
+ fileSize = buffer.length;
498
+
499
+ getChunk = async (start: number, end: number) => {
500
+ return buffer.slice(start, end);
501
+ };
502
+ }
503
+ // Node.js environment with file path
504
+ else if (typeof file === 'string') {
505
+ // Handle Node.js file reading
506
+ if (typeof process === 'undefined' || typeof require !== 'function') {
507
+ throw new Error('File path provided but environment does not support Node.js file system');
508
+ }
509
+
510
+ try {
511
+ // Dynamic import of fs module for Node.js
512
+ const fs = await import('fs/promises');
513
+ const { stat, open } = fs;
514
+
515
+ // Get file size
516
+ const stats = await stat(file);
517
+ fileSize = stats.size;
518
+
519
+ getChunk = async (start: number, end: number) => {
520
+ const fileHandle = await open(file, 'r');
521
+ try {
522
+ const length = end - start;
523
+ const buffer = Buffer.alloc(length);
524
+ await fileHandle.read(buffer, 0, length, start);
525
+ return buffer;
526
+ } finally {
527
+ await fileHandle.close();
528
+ }
529
+ };
530
+ } catch (error: unknown) {
531
+ const errorMessage = error instanceof Error ? error.message : String(error);
532
+ throw new Error(`Failed to read file: ${errorMessage}`);
533
+ }
534
+ }
535
+ else {
536
+ throw new Error('Unsupported file type. Must be File, Blob, Buffer, or string path in Node.js');
537
+ }
538
+
539
+ const chunkSize = Math.ceil(fileSize / totalParts);
540
+
541
+ // Upload each part
542
+ for (let i = 0; i < totalParts; i++) {
543
+ if (abortSignal?.aborted) {
544
+ throw new Error('Upload aborted by user');
545
+ }
546
+
547
+ const partNumber = i + 1;
548
+ const start = i * chunkSize;
549
+ const end = Math.min((i + 1) * chunkSize, fileSize);
550
+
551
+ // Calculate current total progress (based on completed parts so far)
552
+ const currentTotalProgress = calculateTotalProgress();
553
+
554
+ client._log(`[DEBUG] Uploading part ${partNumber}/${totalParts}, bytes ${start}-${end-1} of ${fileSize}, totalPercentComplete: ${currentTotalProgress}%`);
555
+
556
+ // Get the chunk data
557
+ const chunkData = await getChunk(start, end);
558
+
559
+ // Create part-specific options with progress callback
560
+ const partOptions: PartUploadOptions = {
561
+ maxRetries: options.maxRetries,
562
+ abortSignal: abortSignal,
563
+ onPartProgress: onProgress ? (progress) => {
564
+ // Update this part's progress
565
+ partProgress[i] = progress.partPercent;
566
+ const totalPercentComplete = calculateTotalProgress();
567
+
568
+ // Update upload session if enabled
569
+ if (sessionData) {
570
+ try {
571
+ const completedPartsArray = partProgress
572
+ .map((percent, index) => percent === 100 ? index + 1 : null)
573
+ .filter(part => part !== null) as number[];
574
+
575
+ uploadSessionManager.updateProgress(sessionData.projectId, sessionData.sessionId, {
576
+ progress: totalPercentComplete,
577
+ partNumber: progress.partNumber,
578
+ completedParts: completedPartsArray
579
+ });
580
+ } catch (error) {
581
+ console.warn('[SDK] Failed to update upload session progress:', error);
582
+ }
583
+ }
584
+
585
+ // Call user's progress callback with complete information
586
+ onProgress({
587
+ partNumber: progress.partNumber,
588
+ totalParts: progress.totalParts,
589
+ percentComplete: progress.partPercent,
590
+ totalPercentComplete
591
+ });
592
+ } : undefined
593
+ };
594
+
595
+ // Upload the part
596
+ const part = await uploadPart(
597
+ signedUrls[i],
598
+ partNumber,
599
+ totalParts,
600
+ chunkData,
601
+ partOptions
602
+ );
603
+
604
+ completedParts.push(part);
605
+ }
606
+
607
+ // Sort parts by part number to ensure correct order
608
+ completedParts.sort((a, b) => a.PartNumber - b.PartNumber);
609
+
610
+ // Mark upload session as completing if enabled
611
+ if (sessionData) {
612
+ try {
613
+ uploadSessionManager.updateStatus(sessionData.projectId, sessionData.sessionId, 'completing');
614
+ } catch (error) {
615
+ console.warn('[SDK] Failed to update upload session status to completing:', error);
616
+ }
617
+ }
618
+
619
+ return {
620
+ key,
621
+ uploadId,
622
+ parts: completedParts
623
+ };
624
+ } catch (error) {
625
+ // Mark upload session as failed if enabled
626
+ if (sessionData) {
627
+ try {
628
+ const errorMessage = error instanceof Error ? error.message : String(error);
629
+ uploadSessionManager.updateStatus(sessionData.projectId, sessionData.sessionId, 'failed', errorMessage);
630
+ } catch (sessionError) {
631
+ console.warn('[SDK] Failed to update upload session status to failed:', sessionError);
632
+ }
633
+ }
634
+
635
+ // Re-throw the original error
636
+ throw error;
637
+ }
638
+ },
639
+
640
+ /**
641
+ * Gets the page number an asset appears on based on specified filters and sorting.
642
+ * @param {string} assetId - The ID of the asset to find the page for.
643
+ * @param {GetAssetPageParams} [params] - Query parameters for sorting, filtering, and pagination limit.
644
+ * @returns {Promise<AssetPageResponse>} Object containing the page number.
645
+ */
646
+ async getAssetPage(assetId: string, params?: GetAssetPageParams): Promise<AssetPageResponse> {
647
+ if (!assetId) throw new Error('assetId is required.');
648
+ return client._request({
649
+ method: 'GET',
650
+ endpoint: `/v1/assets/page/${assetId}`,
651
+ params: params,
652
+ sendJWT: true,
653
+ });
654
+ },
655
+
656
+ /**
657
+ * Attempts to repair assets (e.g., regenerate signed URLs for pending uploads).
658
+ * @param {string[]} assetIds - An array of asset IDs to repair.
659
+ * @returns {Promise<RepairAssetsResponse>} Array of repair results.
660
+ */
661
+ async repairAssets(assetIds: string[]): Promise<RepairAssetsResponse> {
662
+ if (!assetIds || assetIds.length === 0) throw new Error('assetIds array is required and cannot be empty.');
663
+ return client._request({
664
+ method: 'POST',
665
+ endpoint: '/v1/assets/repair',
666
+ body: { assetIds },
667
+ sendJWT: true,
668
+ });
669
+ },
670
+
671
+ /**
672
+ * Generates signed download URLs for the original files of specified assets.
673
+ * @param {string[]} assetIds - An array of asset IDs.
674
+ * @returns {Promise<DownloadAssetsResponse>} Array of download URL results.
675
+ */
676
+ async downloadAssets(assetIds: string[]): Promise<DownloadAssetsResponse> {
677
+ if (!assetIds || assetIds.length === 0) throw new Error('assetIds array is required and cannot be empty.');
678
+ return client._request({
679
+ method: 'POST',
680
+ endpoint: '/v1/assets/download',
681
+ body: { assetIds },
682
+ sendJWT: true,
683
+ });
684
+ },
685
+
686
+ /**
687
+ * Mints a short-lived signed URL for rendering a document inline.
688
+ *
689
+ * Documents keep their `media` PDF in the private bucket, so unlike images
690
+ * and video it cannot be addressed by keyPath through the file CDN. Fetch
691
+ * this per document open; do not cache it past `expires`.
692
+ *
693
+ * @param {string} assetId - The document asset's ID.
694
+ * @returns {Promise<DocumentViewUrlResponse>} Signed URL, expiry and page count.
695
+ */
696
+ async getDocumentViewUrl(assetId: string): Promise<DocumentViewUrlResponse> {
697
+ if (!assetId) throw new Error('assetId is required.');
698
+ return client._request({
699
+ method: 'GET',
700
+ endpoint: `/v1/assets/${assetId}/document-url`,
701
+ sendJWT: true,
702
+ });
703
+ },
704
+
705
+ /**
706
+ * Tags an asset with a specific tag.
707
+ * @param {string} assetId - The ID of the asset to tag.
708
+ * @param {TagAssetData} tagData - Data containing the tag ID.
709
+ * @returns {Promise<AssetResponse>} The updated asset object.
710
+ */
711
+ async tagAsset(assetId: string, tagData: TagAssetData): Promise<AssetResponse> {
712
+ if (!assetId) throw new Error('assetId is required.');
713
+ if (!tagData.tagId) throw new Error('tagId is required.');
714
+ return client._request<AssetResponse>({
715
+ method: 'PUT',
716
+ endpoint: `/v1/assets/${assetId}/tag`,
717
+ body: tagData,
718
+ sendJWT: true,
719
+ });
720
+ },
721
+
722
+ /**
723
+ * Untags an asset by removing a specific tag.
724
+ * @param {string} assetId - The ID of the asset to untag.
725
+ * @param {UntagAssetData} untagData - Data containing the tag ID to remove.
726
+ * @returns {Promise<AssetResponse>} The updated asset object.
727
+ */
728
+ async untagAsset(assetId: string, untagData: UntagAssetData): Promise<AssetResponse> {
729
+ if (!assetId) throw new Error('assetId is required.');
730
+ if (!untagData.tagId) throw new Error('tagId is required.');
731
+ return client._request<AssetResponse>({
732
+ method: 'PUT',
733
+ endpoint: `/v1/assets/${assetId}/untag`,
734
+ body: untagData,
735
+ sendJWT: true,
736
+ });
737
+ },
738
+
739
+ /**
740
+ * Creates a short link for an asset.
741
+ * If a short link already exists for the asset with the same visibility, returns the existing one.
742
+ * @param {string} assetId - The ID of the asset to create a short link for.
743
+ * @param {CreateAssetShortLinkData} [data] - Optional data including visibility context.
744
+ * @returns {Promise<CreateAssetShortLinkResponse>} The short link object and short URL.
745
+ */
746
+ async createShortLink(assetId: string, data?: CreateAssetShortLinkData): Promise<CreateAssetShortLinkResponse> {
747
+ if (!assetId) throw new Error('assetId is required.');
748
+ return client._request<CreateAssetShortLinkResponse>({
749
+ method: 'POST',
750
+ endpoint: `/v1/assets/${assetId}/shortlink`,
751
+ params: data,
752
+ sendJWT: true,
753
+ });
754
+ },
755
+
756
+ // ========================================================================
757
+ // Public Asset Link Methods
758
+ // ========================================================================
759
+
760
+ /**
761
+ * Create a public download link for an asset.
762
+ * @param {string} assetId - The asset ID.
763
+ * @param {Object} data - Link creation data.
764
+ * @param {string} data.projectId - The project ID.
765
+ * @param {number} [data.validity] - Link validity in milliseconds.
766
+ * @returns {Promise<PublicAssetLink>} The created public link.
767
+ */
768
+ async createPublicLink(
769
+ assetId: string,
770
+ data: { projectId: string; validity?: number; mode?: PublicAssetLinkMode },
771
+ ): Promise<PublicAssetLink> {
772
+ if (!assetId) throw new Error('assetId is required.');
773
+ return client._request<PublicAssetLink>({
774
+ method: 'POST',
775
+ endpoint: `/v1/assets/${assetId}/public-links`,
776
+ body: data,
777
+ sendJWT: true,
778
+ });
779
+ },
780
+
781
+ /**
782
+ * Get all public download links for an asset.
783
+ * @param {string} assetId - The asset ID.
784
+ * @returns {Promise<{ results: PublicAssetLink[] }>} The public links.
785
+ */
786
+ async getPublicLinks(assetId: string, options?: { bypassCache?: boolean }): Promise<{ results: PublicAssetLink[] }> {
787
+ if (!assetId) throw new Error('assetId is required.');
788
+ return client._request<{ results: PublicAssetLink[] }>({
789
+ method: 'GET',
790
+ endpoint: `/v1/assets/${assetId}/public-links`,
791
+ sendJWT: true,
792
+ bypassCache: options?.bypassCache,
793
+ });
794
+ },
795
+
796
+ /**
797
+ * Update a public download link (extend expiration or change status).
798
+ * @param {string} assetId - The asset ID.
799
+ * @param {string} linkId - The link ID.
800
+ * @param {Object} data - Update data.
801
+ * @returns {Promise<PublicAssetLink>} The updated public link.
802
+ */
803
+ async updatePublicLink(
804
+ assetId: string,
805
+ linkId: string,
806
+ data: { validity?: number; status?: string; mode?: PublicAssetLinkMode },
807
+ ): Promise<PublicAssetLink> {
808
+ if (!assetId) throw new Error('assetId is required.');
809
+ if (!linkId) throw new Error('linkId is required.');
810
+ return client._request<PublicAssetLink>({
811
+ method: 'PUT',
812
+ endpoint: `/v1/assets/${assetId}/public-links/${linkId}`,
813
+ body: data,
814
+ sendJWT: true,
815
+ });
816
+ },
817
+
818
+ /**
819
+ * Disable a public download link.
820
+ * @param {string} assetId - The asset ID.
821
+ * @param {string} linkId - The link ID.
822
+ * @returns {Promise<PublicAssetLink>} The disabled public link.
823
+ */
824
+ async disablePublicLink(assetId: string, linkId: string): Promise<PublicAssetLink> {
825
+ if (!assetId) throw new Error('assetId is required.');
826
+ if (!linkId) throw new Error('linkId is required.');
827
+ return client._request<PublicAssetLink>({
828
+ method: 'PUT',
829
+ endpoint: `/v1/assets/${assetId}/public-links/${linkId}/disable`,
830
+ sendJWT: true,
831
+ });
832
+ },
833
+
834
+ /**
835
+ * Reactivate a disabled/expired public download link.
836
+ * @param {string} assetId - The asset ID.
837
+ * @param {string} linkId - The link ID.
838
+ * @param {Object} [data] - Reactivation data.
839
+ * @param {number} [data.validity] - New validity in milliseconds.
840
+ * @returns {Promise<PublicAssetLink>} The reactivated public link.
841
+ */
842
+ async reactivatePublicLink(
843
+ assetId: string,
844
+ linkId: string,
845
+ data?: { validity?: number },
846
+ ): Promise<PublicAssetLink> {
847
+ if (!assetId) throw new Error('assetId is required.');
848
+ if (!linkId) throw new Error('linkId is required.');
849
+ return client._request<PublicAssetLink>({
850
+ method: 'PUT',
851
+ endpoint: `/v1/assets/${assetId}/public-links/${linkId}/reactivate`,
852
+ body: data,
853
+ sendJWT: true,
854
+ });
855
+ },
856
+
857
+ /**
858
+ * Mint a signed multipart upload URL for a user-supplied custom thumbnail
859
+ * image. The upload lands in the originals bucket tagged so the
860
+ * post-processing Lambda generates the customThumbnail outputs and
861
+ * registers them on the asset via the file-update callback.
862
+ *
863
+ * Caller flow:
864
+ * 1. multipartUpload(file, response.urls, response.key, response.uploadId)
865
+ * 2. completeCustomThumbnailUpload({ assetId, key, uploadId, parts })
866
+ * 3. wait for the assetFileUpdate websocket event
867
+ */
868
+ async getCustomThumbnailUploadUrl(
869
+ assetId: string,
870
+ data: { fileName: string; mimeType: string; sizeInMB: number },
871
+ ): Promise<{
872
+ fileName: string;
873
+ assetId: string;
874
+ uploadId: string;
875
+ key: string;
876
+ urls: string[];
877
+ mimeType: string;
878
+ expires: number;
879
+ status: string;
880
+ }> {
881
+ if (!assetId) throw new Error('assetId is required.');
882
+ if (!data?.fileName || !data?.mimeType || !data?.sizeInMB) {
883
+ throw new Error('fileName, mimeType, and sizeInMB are required.');
884
+ }
885
+ return client._request({
886
+ method: 'POST',
887
+ endpoint: `/v1/assets/${assetId}/custom-thumbnail/upload-url`,
888
+ body: data,
889
+ sendJWT: true,
890
+ });
891
+ },
892
+
893
+ /**
894
+ * Finalize the multipart S3 upload for a custom thumbnail. Triggers the
895
+ * post-processing Lambda by committing the S3 object.
896
+ */
897
+ async completeCustomThumbnailUpload(
898
+ assetId: string,
899
+ data: { key: string; uploadId: string; parts: { ETag: string; PartNumber: number }[] },
900
+ ): Promise<{ status: string; key: string }> {
901
+ if (!assetId) throw new Error('assetId is required.');
902
+ if (!data?.key || !data?.uploadId || !data?.parts?.length) {
903
+ throw new Error('key, uploadId, and parts are required.');
904
+ }
905
+ return client._request({
906
+ method: 'POST',
907
+ endpoint: `/v1/assets/${assetId}/custom-thumbnail/complete-upload`,
908
+ body: data,
909
+ sendJWT: true,
910
+ });
911
+ },
912
+
913
+ /**
914
+ * Remove the custom thumbnail from an asset. Soft-deletes all custom
915
+ * thumb files; the asset falls back to the auto-generated thumbnail.
916
+ */
917
+ async removeCustomThumbnail(assetId: string): Promise<Asset> {
918
+ if (!assetId) throw new Error('assetId is required.');
919
+ return client._request<Asset>({
920
+ method: 'DELETE',
921
+ endpoint: `/v1/assets/${assetId}/custom-thumbnail`,
922
+ sendJWT: true,
923
+ });
924
+ },
925
+
926
+ /**
927
+ * Promote a chat-message attachment into a project as a fresh,
928
+ * independent project asset. The source attachment is left untouched;
929
+ * the new project asset has its own lifecycle, post-processing
930
+ * pipeline, and storage footprint.
931
+ *
932
+ * Idempotent: a second promote of the same source into the same
933
+ * project returns the existing promoted asset with `deduped: true`.
934
+ *
935
+ * Requires `canCreateAsset` on the destination project — reviewers
936
+ * are blocked. The server additionally rejects when the source
937
+ * attachment's workspace doesn't match the destination project's.
938
+ */
939
+ async promoteAttachmentToProject(
940
+ assetId: string,
941
+ payload: { projectId: string; fileName?: string },
942
+ ): Promise<{ asset: Asset; deduped: boolean }> {
943
+ if (!assetId) throw new Error('assetId is required.');
944
+ if (!payload?.projectId) throw new Error('projectId is required.');
945
+ return client._request<{ asset: Asset; deduped: boolean }>({
946
+ method: 'POST',
947
+ endpoint: `/v1/assets/${assetId}/promote-to-project`,
948
+ body: payload,
949
+ sendJWT: true,
950
+ });
951
+ },
952
+
953
+ /**
954
+ * Upload Session Management Methods
955
+ */
956
+
957
+ /**
958
+ * Get all active upload sessions for a project
959
+ * @param {string} projectId - The project ID to get sessions for
960
+ * @returns {UploadSessionData[]} Array of upload session data
961
+ */
962
+ getUploadSessions(projectId: string) {
963
+ if (!projectId) throw new Error('projectId is required');
964
+ return uploadSessionManager.getProjectSessions(projectId);
965
+ },
966
+
967
+ /**
968
+ * True when any upload is genuinely in flight anywhere in the app (across
969
+ * all projects and tabs). Intended for app-level guards — e.g. suppressing
970
+ * an automatic version-update page refresh while bytes are still uploading.
971
+ * Stale (crashed-tab) sessions are ignored via the freshness window.
972
+ * @param {number} [staleMs] - Max age of the last progress update that still
973
+ * counts as active (default: 2 minutes)
974
+ * @returns {boolean}
975
+ */
976
+ hasActiveUploads(staleMs?: number) {
977
+ return uploadSessionManager.hasActiveUploads(staleMs);
978
+ },
979
+
980
+ /**
981
+ * Get a specific upload session
982
+ * @param {string} projectId - The project ID
983
+ * @param {string} sessionId - The session ID
984
+ * @returns {UploadSessionData | null} Upload session data or null if not found
985
+ */
986
+ getUploadSession(projectId: string, sessionId: string) {
987
+ if (!projectId) throw new Error('projectId is required');
988
+ if (!sessionId) throw new Error('sessionId is required');
989
+ return uploadSessionManager.getSession(projectId, sessionId);
990
+ },
991
+
992
+ /**
993
+ * Remove an upload session
994
+ * @param {string} projectId - The project ID
995
+ * @param {string} sessionId - The session ID
996
+ */
997
+ removeUploadSession(projectId: string, sessionId: string) {
998
+ if (!projectId) throw new Error('projectId is required');
999
+ if (!sessionId) throw new Error('sessionId is required');
1000
+ uploadSessionManager.removeSession(projectId, sessionId);
1001
+ },
1002
+
1003
+ /**
1004
+ * Clean up old upload sessions for a project
1005
+ * @param {string} projectId - The project ID
1006
+ * @param {number} [olderThanMs] - Remove sessions older than this (default: 24 hours)
1007
+ */
1008
+ cleanupUploadSessions(projectId: string, olderThanMs?: number) {
1009
+ if (!projectId) throw new Error('projectId is required');
1010
+ uploadSessionManager.cleanupSessions(projectId, olderThanMs);
1011
+ },
1012
+
1013
+ /**
1014
+ * Register a listener for cross-tab upload session messages
1015
+ * @param {string} listenerId - Unique listener ID
1016
+ * @param {function} callback - Callback function to handle messages
1017
+ */
1018
+ onUploadSessionMessage(listenerId: string, callback: (message: any) => void) {
1019
+ if (!listenerId) throw new Error('listenerId is required');
1020
+ if (typeof callback !== 'function') throw new Error('callback must be a function');
1021
+ uploadSessionManager.onMessage(listenerId, callback);
1022
+ },
1023
+
1024
+ /**
1025
+ * Unregister a cross-tab upload session message listener
1026
+ * @param {string} listenerId - Unique listener ID
1027
+ */
1028
+ offUploadSessionMessage(listenerId: string) {
1029
+ if (!listenerId) throw new Error('listenerId is required');
1030
+ uploadSessionManager.offMessage(listenerId);
1031
+ },
1032
+
1033
+ /**
1034
+ * Mark an upload session as completed
1035
+ * @param {string} projectId - The project ID
1036
+ * @param {string} sessionId - The session ID
1037
+ */
1038
+ completeUploadSession(projectId: string, sessionId: string) {
1039
+ if (!projectId) throw new Error('projectId is required');
1040
+ if (!sessionId) throw new Error('sessionId is required');
1041
+ uploadSessionManager.updateStatus(projectId, sessionId, 'completed');
1042
+ },
1043
+
1044
+ // ========================================================================
1045
+ // Access Activity Methods (play / download / embed metrics)
1046
+ // ========================================================================
1047
+
1048
+ /**
1049
+ * Record an authenticated play event from the in-app player. Fire-and-forget;
1050
+ * server returns 204. Throw-on-failure is fine because the caller already
1051
+ * de-dupes per session.
1052
+ */
1053
+ async recordAccessActivity(
1054
+ assetId: string,
1055
+ body: { eventType: 'play_started' | 'play_completed'; visibility: 'creator' | 'reviewer' },
1056
+ ): Promise<void> {
1057
+ if (!assetId) throw new Error('assetId is required.');
1058
+ await client._request<void>({
1059
+ method: 'POST',
1060
+ endpoint: `/v1/assets/${assetId}/access-activity`,
1061
+ body,
1062
+ sendJWT: true,
1063
+ });
1064
+ },
1065
+
1066
+ /**
1067
+ * Get aggregated access-activity for a single asset. Returns totals per
1068
+ * eventType, a breakdown for the requested dimension, and a zero-filled
1069
+ * daily series.
1070
+ */
1071
+ async getAssetAccessActivity(
1072
+ assetId: string,
1073
+ params?: {
1074
+ range?: '7d' | '30d' | '90d';
1075
+ groupBy?: 'visibility' | 'referrerHost' | 'country' | 'userAgentClass' | 'day';
1076
+ eventType?: 'play_started' | 'play_completed' | 'download' | 'embed_resolved';
1077
+ limit?: number;
1078
+ },
1079
+ ): Promise<{
1080
+ range: string;
1081
+ from: string;
1082
+ to: string;
1083
+ totals: Array<{ eventType: string; count: number }>;
1084
+ breakdown: Array<{ value: string; label: string; count: number }>;
1085
+ series: Array<{ date: string; eventType: string; count: number }>;
1086
+ groupBy: string;
1087
+ eventType: string | null;
1088
+ }> {
1089
+ if (!assetId) throw new Error('assetId is required.');
1090
+ const query = new URLSearchParams();
1091
+ if (params?.range) query.set('range', params.range);
1092
+ if (params?.groupBy) query.set('groupBy', params.groupBy);
1093
+ if (params?.eventType) query.set('eventType', params.eventType);
1094
+ if (params?.limit != null) query.set('limit', String(params.limit));
1095
+ const qs = query.toString();
1096
+ return client._request({
1097
+ method: 'GET',
1098
+ endpoint: `/v1/assets/${assetId}/access-activity${qs ? `?${qs}` : ''}`,
1099
+ sendJWT: true,
1100
+ });
1101
+ },
1102
+
1103
+ };
1104
+ }