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