streamby-core 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +141 -44
- package/dist/adapters/database/connectionManager.js +24 -11
- package/dist/adapters/storage/s3.d.ts +3 -2
- package/dist/adapters/storage/s3.js +18 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/mcp/server.d.ts +4 -3
- package/dist/mcp/server.js +25 -10
- package/dist/mcp/tools.d.ts +7 -2
- package/dist/mcp/tools.js +187 -35
- package/dist/mcp/workflowNodes.d.ts +24 -0
- package/dist/mcp/workflowNodes.js +48 -0
- package/dist/middleware/createRouter.js +2 -0
- package/dist/middleware/routes/apiToken.js +77 -14
- package/dist/middleware/routes/deliverable.d.ts +3 -0
- package/dist/middleware/routes/deliverable.js +131 -0
- package/dist/middleware/routes/export.js +2 -0
- package/dist/middleware/routes/member.js +8 -0
- package/dist/middleware/routes/production.js +60 -67
- package/dist/middleware/routes/project.js +15 -3
- package/dist/middleware/routes/storage.js +115 -24
- package/dist/middleware/routes/subscription.js +3 -0
- package/dist/services/apiTokenPermissions.d.ts +16 -0
- package/dist/services/apiTokenPermissions.js +102 -0
- package/dist/services/apiTokens.d.ts +38 -14
- package/dist/services/apiTokens.js +134 -40
- package/dist/services/assetVersions.d.ts +48 -0
- package/dist/services/assetVersions.js +84 -0
- package/dist/services/auth.d.ts +4 -1
- package/dist/services/auth.js +24 -18
- package/dist/services/deliverables.d.ts +26 -0
- package/dist/services/deliverables.js +55 -0
- package/dist/services/mediaProcessor.js +3 -1
- package/dist/services/production.d.ts +2 -0
- package/dist/services/production.js +13 -0
- package/dist/services/productionFields.d.ts +16 -0
- package/dist/services/productionFields.js +80 -0
- package/dist/services/signedUrls.d.ts +6 -0
- package/dist/services/signedUrls.js +9 -0
- package/dist/types/index.d.ts +12 -4
- package/dist/utils/auth.d.ts +12 -1
- package/dist/utils/auth.js +23 -2
- package/dist/utils/nosqlDb.d.ts +2 -0
- package/dist/utils/nosqlDb.js +11 -0
- package/package.json +1 -1
|
@@ -15,6 +15,9 @@ const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
|
15
15
|
const connectionResolver_1 = require("../../services/connectionResolver");
|
|
16
16
|
const mongodb_1 = require("mongodb");
|
|
17
17
|
const crypto_1 = __importDefault(require("crypto"));
|
|
18
|
+
const signedUrls_1 = require("../../services/signedUrls");
|
|
19
|
+
const assetVersions_1 = require("../../services/assetVersions");
|
|
20
|
+
const deliverables_1 = require("../../services/deliverables");
|
|
18
21
|
const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
19
22
|
const VALID_3D_EXTENSIONS = new Set(['.glb', '.gltf', '.obj', '.fbx', '.stl', '.ply']);
|
|
20
23
|
function validateContentType(contentType, filename, category) {
|
|
@@ -135,7 +138,7 @@ function storageRouter(config) {
|
|
|
135
138
|
const storageKey = `${projectId}/${category}/${fileId}${ext}`;
|
|
136
139
|
const collection = getRawFilesCollection();
|
|
137
140
|
if (collection) {
|
|
138
|
-
|
|
141
|
+
const fileDoc = {
|
|
139
142
|
_id: new mongodb_1.ObjectId(),
|
|
140
143
|
fileId,
|
|
141
144
|
projectId,
|
|
@@ -145,10 +148,13 @@ function storageRouter(config) {
|
|
|
145
148
|
contentType,
|
|
146
149
|
uploadedBy: auth.userId,
|
|
147
150
|
createdAt: new Date(),
|
|
148
|
-
}
|
|
151
|
+
};
|
|
152
|
+
await collection.insertOne(fileDoc);
|
|
153
|
+
await (0, assetVersions_1.recordFirstVersion)(fileDoc);
|
|
149
154
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
155
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
156
|
+
const url = await adapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
|
|
157
|
+
res.json({ url, fileId, storageKey, displayName: fileName, ...expiry(expiresIn) });
|
|
152
158
|
}
|
|
153
159
|
catch (err) {
|
|
154
160
|
res.status(500).json({ message: 'Failed to generate upload URL', details: err.message });
|
|
@@ -165,7 +171,7 @@ function storageRouter(config) {
|
|
|
165
171
|
const project = await Project.findOne({ _id: projectId });
|
|
166
172
|
if (!project)
|
|
167
173
|
return res.status(404).json({ message: 'Project not found' });
|
|
168
|
-
if (!(0, auth_1.
|
|
174
|
+
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
169
175
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
170
176
|
const collection = getRawFilesCollection();
|
|
171
177
|
if (!collection)
|
|
@@ -179,9 +185,21 @@ function storageRouter(config) {
|
|
|
179
185
|
if (!adapter.getPresignedUploadUrl) {
|
|
180
186
|
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
181
187
|
}
|
|
182
|
-
|
|
188
|
+
// The new version goes to the asset's own key, so its URL doesn't change; the current
|
|
189
|
+
// one is archived first, and without a way to archive it nothing is replaced.
|
|
190
|
+
if (!adapter.copyFile)
|
|
191
|
+
return res.status(501).json({ message: 'Storage adapter cannot keep versions' });
|
|
192
|
+
let version;
|
|
193
|
+
try {
|
|
194
|
+
version = await (0, assetVersions_1.archiveCurrentVersion)(fileDoc, { fileName, contentType, userId: auth.userId }, adapter.copyFile.bind(adapter));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
return res.status(502).json({ message: 'Failed to archive the current version; nothing was replaced', details: err.message });
|
|
198
|
+
}
|
|
199
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
200
|
+
const url = await adapter.getPresignedUploadUrl(fileDoc.storageKey, contentType, expiresIn);
|
|
183
201
|
await collection.updateOne({ fileId, projectId }, { $set: { contentType, updatedAt: new Date() } });
|
|
184
|
-
res.json({ url, storageKey: fileDoc.storageKey, fileId });
|
|
202
|
+
res.json({ url, storageKey: fileDoc.storageKey, fileId, versionId: version.versionId, version: version.version, ...expiry(expiresIn) });
|
|
185
203
|
}
|
|
186
204
|
catch (err) {
|
|
187
205
|
res.status(500).json({ message: 'Failed to generate replace URL', details: err.message });
|
|
@@ -227,8 +245,7 @@ function storageRouter(config) {
|
|
|
227
245
|
const fileDoc = await collection.findOne({ fileId, projectId });
|
|
228
246
|
if (!fileDoc)
|
|
229
247
|
return res.status(404).json({ message: 'File not found' });
|
|
230
|
-
|
|
231
|
-
await adapter.deleteFile(fileDoc.storageKey);
|
|
248
|
+
await deleteAsset(fileDoc, adapter);
|
|
232
249
|
await collection.deleteOne({ fileId, projectId });
|
|
233
250
|
res.json({ message: 'File deleted successfully' });
|
|
234
251
|
}
|
|
@@ -249,7 +266,7 @@ function storageRouter(config) {
|
|
|
249
266
|
if (!collection)
|
|
250
267
|
return res.status(500).json({ message: 'Database not available' });
|
|
251
268
|
const files = await collection.find({ projectId }).sort({ createdAt: -1 }).limit(12).toArray();
|
|
252
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter)));
|
|
269
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
253
270
|
res.json({ data });
|
|
254
271
|
}
|
|
255
272
|
catch (err) {
|
|
@@ -272,13 +289,61 @@ function storageRouter(config) {
|
|
|
272
289
|
if (!collection)
|
|
273
290
|
return res.status(500).json({ message: 'Database not available' });
|
|
274
291
|
const files = await collection.find({ projectId, category }).sort({ createdAt: -1 }).toArray();
|
|
275
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter)));
|
|
292
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
276
293
|
res.json({ data });
|
|
277
294
|
}
|
|
278
295
|
catch (err) {
|
|
279
296
|
res.status(500).json({ message: 'Failed to list files', details: err.message });
|
|
280
297
|
}
|
|
281
298
|
});
|
|
299
|
+
// A short-lived URL to download an asset, or one of its versions, wherever it's stored
|
|
300
|
+
// (the built-in storage or one of the project's storage connections). It's for that file
|
|
301
|
+
// only, and downloads it with its name.
|
|
302
|
+
router.get('/projects/:projectId/assets/:fileId/download-url', async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const auth = req.auth;
|
|
305
|
+
const { projectId, fileId } = req.params;
|
|
306
|
+
const { versionId } = req.query;
|
|
307
|
+
const project = await Project.findOne({ _id: projectId });
|
|
308
|
+
if (!project)
|
|
309
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
310
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
311
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
312
|
+
const collection = getRawFilesCollection();
|
|
313
|
+
if (!collection)
|
|
314
|
+
return res.status(500).json({ message: 'Database not available' });
|
|
315
|
+
const fileDoc = await collection.findOne({ fileId, projectId });
|
|
316
|
+
if (!fileDoc)
|
|
317
|
+
return res.status(404).json({ message: 'File not found' });
|
|
318
|
+
let storageKey = fileDoc.storageKey;
|
|
319
|
+
let fileName = fileDoc.displayName;
|
|
320
|
+
let contentType = fileDoc.contentType;
|
|
321
|
+
let version;
|
|
322
|
+
if (versionId) {
|
|
323
|
+
const found = await (0, assetVersions_1.findVersion)(projectId, fileId, versionId);
|
|
324
|
+
if (!found)
|
|
325
|
+
return res.status(404).json({ message: 'Version not found' });
|
|
326
|
+
storageKey = found.storageKey;
|
|
327
|
+
fileName = found.fileName ?? fileName;
|
|
328
|
+
contentType = found.contentType ?? contentType;
|
|
329
|
+
version = found.version;
|
|
330
|
+
}
|
|
331
|
+
// Its storage connection's adapter; files stored before connections had their own row
|
|
332
|
+
// (no connection, or the built-in provider's id) are in the default storage.
|
|
333
|
+
const connId = fileDoc.storageConnectionId;
|
|
334
|
+
const fileAdapter = connId && findStorageConn(project, connId) ? await resolveConnAdapter(connId, project, auth) : adapter;
|
|
335
|
+
if ('error' in fileAdapter)
|
|
336
|
+
return res.status(fileAdapter.status).json({ message: fileAdapter.error });
|
|
337
|
+
if (!fileAdapter.getPresignedGetUrl)
|
|
338
|
+
return res.status(501).json({ message: 'Storage adapter does not support download URLs' });
|
|
339
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'download') ?? 3600;
|
|
340
|
+
const url = await fileAdapter.getPresignedGetUrl(storageKey, expiresIn, fileName);
|
|
341
|
+
res.json({ url, fileId, fileName, contentType: contentType ?? null, ...(version ? { versionId, version } : {}), ...expiry(expiresIn) });
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
res.status(500).json({ message: 'Failed to generate download URL', details: err.message });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
282
347
|
// ─── Per-connection file routes ───────────────────────────────────────────
|
|
283
348
|
router.get('/projects/:projectId/connections/storage/:connId/upload-url', async (req, res) => {
|
|
284
349
|
try {
|
|
@@ -309,7 +374,7 @@ function storageRouter(config) {
|
|
|
309
374
|
const storageKey = `${projectId}/${category}/${fileId}${ext}`;
|
|
310
375
|
const collection = getRawFilesCollection();
|
|
311
376
|
if (collection) {
|
|
312
|
-
|
|
377
|
+
const fileDoc = {
|
|
313
378
|
_id: new mongodb_1.ObjectId(),
|
|
314
379
|
fileId,
|
|
315
380
|
projectId,
|
|
@@ -320,10 +385,13 @@ function storageRouter(config) {
|
|
|
320
385
|
contentType,
|
|
321
386
|
uploadedBy: auth.userId,
|
|
322
387
|
createdAt: new Date(),
|
|
323
|
-
}
|
|
388
|
+
};
|
|
389
|
+
await collection.insertOne(fileDoc);
|
|
390
|
+
await (0, assetVersions_1.recordFirstVersion)(fileDoc);
|
|
324
391
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
392
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
393
|
+
const url = await connAdapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
|
|
394
|
+
res.json({ url, fileId, storageKey, displayName: fileName, ...expiry(expiresIn) });
|
|
327
395
|
}
|
|
328
396
|
catch (err) {
|
|
329
397
|
res.status(500).json({ message: 'Failed to generate upload URL', details: err.message });
|
|
@@ -345,7 +413,7 @@ function storageRouter(config) {
|
|
|
345
413
|
if (!collection)
|
|
346
414
|
return res.status(500).json({ message: 'Database not available' });
|
|
347
415
|
const files = await collection.find(connFilter(projectId, connId, project)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
348
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
416
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
349
417
|
res.json({ data });
|
|
350
418
|
}
|
|
351
419
|
catch (err) {
|
|
@@ -371,7 +439,7 @@ function storageRouter(config) {
|
|
|
371
439
|
if (!collection)
|
|
372
440
|
return res.status(500).json({ message: 'Database not available' });
|
|
373
441
|
const files = await collection.find(connFilter(projectId, connId, project, { category })).sort({ createdAt: -1 }).toArray();
|
|
374
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
442
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
375
443
|
res.json({ data });
|
|
376
444
|
}
|
|
377
445
|
catch (err) {
|
|
@@ -389,7 +457,7 @@ function storageRouter(config) {
|
|
|
389
457
|
const project = await Project.findOne({ _id: projectId });
|
|
390
458
|
if (!project)
|
|
391
459
|
return res.status(404).json({ message: 'Project not found' });
|
|
392
|
-
if (!(0, auth_1.
|
|
460
|
+
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
393
461
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
394
462
|
const collection = getRawFilesCollection();
|
|
395
463
|
if (!collection)
|
|
@@ -406,9 +474,21 @@ function storageRouter(config) {
|
|
|
406
474
|
if (!connAdapter.getPresignedUploadUrl) {
|
|
407
475
|
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
408
476
|
}
|
|
409
|
-
|
|
477
|
+
// The new version goes to the asset's own key, so its URL doesn't change; the current
|
|
478
|
+
// one is archived first, and without a way to archive it nothing is replaced.
|
|
479
|
+
if (!connAdapter.copyFile)
|
|
480
|
+
return res.status(501).json({ message: 'Storage adapter cannot keep versions' });
|
|
481
|
+
let version;
|
|
482
|
+
try {
|
|
483
|
+
version = await (0, assetVersions_1.archiveCurrentVersion)(fileDoc, { fileName, contentType, userId: auth.userId }, connAdapter.copyFile.bind(connAdapter));
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
return res.status(502).json({ message: 'Failed to archive the current version; nothing was replaced', details: err.message });
|
|
487
|
+
}
|
|
488
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
489
|
+
const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType, expiresIn);
|
|
410
490
|
await collection.updateOne(connFilter(projectId, connId, project, { fileId }), { $set: { contentType, updatedAt: new Date() } });
|
|
411
|
-
res.json({ url, storageKey: fileDoc.storageKey, fileId });
|
|
491
|
+
res.json({ url, storageKey: fileDoc.storageKey, fileId, versionId: version.versionId, version: version.version, ...expiry(expiresIn) });
|
|
412
492
|
}
|
|
413
493
|
catch (err) {
|
|
414
494
|
res.status(500).json({ message: 'Failed to generate replace URL', details: err.message });
|
|
@@ -457,8 +537,7 @@ function storageRouter(config) {
|
|
|
457
537
|
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
458
538
|
if (!fileDoc)
|
|
459
539
|
return res.status(404).json({ message: 'File not found' });
|
|
460
|
-
|
|
461
|
-
await connAdapter.deleteFile(fileDoc.storageKey);
|
|
540
|
+
await deleteAsset(fileDoc, connAdapter);
|
|
462
541
|
await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
|
|
463
542
|
res.json({ message: 'File deleted successfully' });
|
|
464
543
|
}
|
|
@@ -731,11 +810,11 @@ function storageRouter(config) {
|
|
|
731
810
|
});
|
|
732
811
|
return router;
|
|
733
812
|
}
|
|
734
|
-
async function resolveFileUrl(file, adapter) {
|
|
813
|
+
async function resolveFileUrl(file, adapter, expiresIn) {
|
|
735
814
|
let url = null;
|
|
736
815
|
if (adapter.getPresignedGetUrl) {
|
|
737
816
|
try {
|
|
738
|
-
url = await adapter.getPresignedGetUrl(file.storageKey);
|
|
817
|
+
url = await adapter.getPresignedGetUrl(file.storageKey, expiresIn);
|
|
739
818
|
}
|
|
740
819
|
catch { /* leave null */ }
|
|
741
820
|
}
|
|
@@ -751,3 +830,15 @@ async function resolveFileUrl(file, adapter) {
|
|
|
751
830
|
url,
|
|
752
831
|
};
|
|
753
832
|
}
|
|
833
|
+
// When a signed URL stops working, for a client to know (only when it's short-lived).
|
|
834
|
+
const expiry = (expiresIn) => expiresIn ? { expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString() } : {};
|
|
835
|
+
// An asset's files (every version), its versions and its deliverables. Its record is the
|
|
836
|
+
// caller's to delete.
|
|
837
|
+
async function deleteAsset(fileDoc, adapter) {
|
|
838
|
+
const keys = await (0, assetVersions_1.versionStorageKeys)(fileDoc.projectId, fileDoc.fileId, fileDoc.storageKey);
|
|
839
|
+
if (adapter.deleteFile)
|
|
840
|
+
for (const key of keys)
|
|
841
|
+
await adapter.deleteFile(key);
|
|
842
|
+
await (0, assetVersions_1.deleteVersions)(fileDoc.projectId, fileDoc.fileId);
|
|
843
|
+
await (0, deliverables_1.deleteDeliverablesOf)(fileDoc.projectId, { assetId: fileDoc.fileId });
|
|
844
|
+
}
|
|
@@ -18,6 +18,9 @@ const ensureSubscription = async (req, res, next) => {
|
|
|
18
18
|
const auth = req.auth;
|
|
19
19
|
if (!auth)
|
|
20
20
|
return next();
|
|
21
|
+
// A project's token is no user: it has no plan of its own.
|
|
22
|
+
if (auth.apiToken?.kind === 'project')
|
|
23
|
+
return next();
|
|
21
24
|
const plan = await resolveSubscription(auth);
|
|
22
25
|
req.subscription = plan;
|
|
23
26
|
next();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Permissions on a project, for the projects a token reaches. */
|
|
2
|
+
export declare const PROJECT_PERMISSIONS: readonly ["project:read", "project:update", "project:delete", "members:read", "members:manage", "assets:read", "assets:write", "assets:delete", "production:read", "production:write", "production:delete", "workflow:read", "workflow:write", "exports:read", "exports:write", "rights:read", "rights:write", "jobs:read", "jobs:run"];
|
|
3
|
+
/** Permissions on the account: only a user's token has them. */
|
|
4
|
+
export declare const ACCOUNT_PERMISSIONS: readonly ["projects:create"];
|
|
5
|
+
export type ProjectPermission = typeof PROJECT_PERMISSIONS[number];
|
|
6
|
+
export type AccountPermission = typeof ACCOUNT_PERMISSIONS[number];
|
|
7
|
+
export type ApiTokenPermission = ProjectPermission | AccountPermission;
|
|
8
|
+
export declare const isProjectPermission: (value: unknown) => value is ProjectPermission;
|
|
9
|
+
export declare const isAccountPermission: (value: unknown) => value is AccountPermission;
|
|
10
|
+
/** What a route needs of a token: a permission, or only being a token (`null`). */
|
|
11
|
+
export interface RouteRule {
|
|
12
|
+
permission: ApiTokenPermission | null;
|
|
13
|
+
userTokenOnly?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** The rule for a request made with an API token, or undefined: not reachable with one. */
|
|
16
|
+
export declare function routeRuleFor(method: string, path: string): RouteRule | undefined;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What an API token may do: a list of permissions, each a resource and an action. A request
|
|
3
|
+
// made with a token goes through only when its route's permission is among the token's (see
|
|
4
|
+
// enforceApiTokenScope), and a route without one isn't reachable with a token at all.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isAccountPermission = exports.isProjectPermission = exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = void 0;
|
|
7
|
+
exports.routeRuleFor = routeRuleFor;
|
|
8
|
+
/** Permissions on a project, for the projects a token reaches. */
|
|
9
|
+
exports.PROJECT_PERMISSIONS = [
|
|
10
|
+
'project:read', 'project:update', 'project:delete',
|
|
11
|
+
'members:read', 'members:manage',
|
|
12
|
+
'assets:read', 'assets:write', 'assets:delete',
|
|
13
|
+
'production:read', 'production:write', 'production:delete',
|
|
14
|
+
'workflow:read', 'workflow:write',
|
|
15
|
+
'exports:read', 'exports:write',
|
|
16
|
+
'rights:read', 'rights:write',
|
|
17
|
+
'jobs:read', 'jobs:run',
|
|
18
|
+
];
|
|
19
|
+
/** Permissions on the account: only a user's token has them. */
|
|
20
|
+
exports.ACCOUNT_PERMISSIONS = ['projects:create'];
|
|
21
|
+
const isProjectPermission = (value) => exports.PROJECT_PERMISSIONS.includes(value);
|
|
22
|
+
exports.isProjectPermission = isProjectPermission;
|
|
23
|
+
const isAccountPermission = (value) => exports.ACCOUNT_PERMISSIONS.includes(value);
|
|
24
|
+
exports.isAccountPermission = isAccountPermission;
|
|
25
|
+
const READ = ['GET', 'HEAD', 'OPTIONS'];
|
|
26
|
+
const methodsOf = (methods) => methods === 'read' ? READ : methods === 'write' ? ['POST', 'PUT', 'PATCH'] : methods === 'delete' ? ['DELETE'] : methods;
|
|
27
|
+
// A project's own path (not /projects/create nor /projects/explore).
|
|
28
|
+
const P = '^/projects/(?!(?:create|explore)/?$)[^/]+';
|
|
29
|
+
const rule = (methods, path, permission, extra = {}) => ({ methods: methodsOf(methods), pattern: new RegExp(`${path}/?$`), permission, ...extra });
|
|
30
|
+
// The first rule that matches a request decides it; a request no rule matches is refused.
|
|
31
|
+
// Connections, integrations and allowed origins are the project's settings (project:*);
|
|
32
|
+
// reviews, QC reports and dependency graphs are of its assets; pipelines are workflow.
|
|
33
|
+
const RULES = [
|
|
34
|
+
// Outside a project.
|
|
35
|
+
rule('read', '^/auth', null),
|
|
36
|
+
rule('read', '^/projects', null),
|
|
37
|
+
rule(['POST'], '^/projects/create', 'projects:create'),
|
|
38
|
+
rule('read', '^/user/subscription', null, { userTokenOnly: true }),
|
|
39
|
+
rule('read', '^/users/search', 'members:manage'),
|
|
40
|
+
rule('read', '^/jobs/[^/]+', 'jobs:read'),
|
|
41
|
+
// The project itself and its settings.
|
|
42
|
+
rule('read', `${P}(/preview)?`, 'project:read'),
|
|
43
|
+
rule(['PATCH'], P, 'project:update'),
|
|
44
|
+
rule('delete', P, 'project:delete'),
|
|
45
|
+
rule(['PATCH'], `${P}/(archive|unarchive)`, 'project:update', { userTokenOnly: true }),
|
|
46
|
+
rule(['POST'], `${P}/image`, 'project:update'),
|
|
47
|
+
rule(['PATCH'], `${P}/origins`, 'project:update'),
|
|
48
|
+
rule(['POST'], `${P}/integrations`, 'project:update'),
|
|
49
|
+
rule('delete', `${P}/integrations/[^/]+`, 'project:update'),
|
|
50
|
+
rule('read', '^/upload-project-image-url/[^/]+', 'project:update'),
|
|
51
|
+
rule('delete', '^/delete-project-image-url/[^/]+', 'project:update'),
|
|
52
|
+
// Members.
|
|
53
|
+
rule('read', `${P}/members`, 'members:read'),
|
|
54
|
+
rule(['POST'], `${P}/members`, 'members:manage'),
|
|
55
|
+
rule(['PATCH'], `${P}/members/[^/]+(/approve)?`, 'members:manage'),
|
|
56
|
+
rule('delete', `${P}/members/[^/]+`, 'members:manage'),
|
|
57
|
+
// Assets: the built-in storage, and a storage connection's files and folders.
|
|
58
|
+
rule('read', `${P}/(storage|connections/storage/[^/]+)/(.+/)?(upload-url|replace-url)`, 'assets:write'),
|
|
59
|
+
rule('read', `${P}/storage/.+`, 'assets:read'),
|
|
60
|
+
rule(['PATCH'], `${P}/storage/files/[^/]+`, 'assets:write'),
|
|
61
|
+
rule('delete', `${P}/storage/files/[^/]+`, 'assets:delete'),
|
|
62
|
+
rule('read', `${P}/connections/storage/[^/]+/(files|folders)(/.+)?`, 'assets:read'),
|
|
63
|
+
rule(['POST', 'PATCH'], `${P}/connections/storage/[^/]+/(files|folders)(/.+)?`, 'assets:write'),
|
|
64
|
+
rule('delete', `${P}/connections/storage/[^/]+/(files|folders)/[^/]+`, 'assets:delete'),
|
|
65
|
+
rule('read', `${P}/assets/[^/]+/(metadata|versions|qc-report|dependency-graph|download-url)`, 'assets:read'),
|
|
66
|
+
rule(['PATCH'], `${P}/assets/[^/]+/metadata`, 'assets:write'),
|
|
67
|
+
rule(['POST'], `${P}/assets/[^/]+/qc-report`, 'assets:write'),
|
|
68
|
+
rule('read', `${P}/(reviews(/[^/]+)?|assets/[^/]+/versions/[^/]+/annotations)`, 'assets:read'),
|
|
69
|
+
rule(['POST', 'PATCH'], `${P}/(reviews(/[^/]+/decision)?|assets/[^/]+/versions/[^/]+/annotations(/[^/]+/resolve)?)`, 'assets:write'),
|
|
70
|
+
// Deliverables: an asset (or a version) delivered for a shot or an export.
|
|
71
|
+
rule('read', `${P}/deliverables`, 'assets:read'),
|
|
72
|
+
rule(['POST'], `${P}/deliverables`, 'assets:write'),
|
|
73
|
+
rule('delete', `${P}/deliverables/[^/]+`, 'assets:write'),
|
|
74
|
+
// Rights.
|
|
75
|
+
rule('read', `${P}/assets/[^/]+/rights`, 'rights:read'),
|
|
76
|
+
rule(['PUT'], `${P}/assets/[^/]+/rights`, 'rights:write'),
|
|
77
|
+
// Connections (API, database, storage, distribution, render farm): the project's settings.
|
|
78
|
+
rule('read', `${P}/(connections|get-connection|distribution-connections|render-farm-connections)(/.+)?`, 'project:read'),
|
|
79
|
+
rule('write', `${P}/(connections|distribution-connections|render-farm-connections)(/.+)?`, 'project:update'),
|
|
80
|
+
rule('delete', `${P}/(connections|distribution-connections|render-farm-connections)/.+`, 'project:update'),
|
|
81
|
+
// Production.
|
|
82
|
+
rule('read', `${P}/production/.+`, 'production:read'),
|
|
83
|
+
rule('write', `${P}/production/.+`, 'production:write'),
|
|
84
|
+
rule('delete', `${P}/production/.+`, 'production:delete'),
|
|
85
|
+
// Workflow and pipelines.
|
|
86
|
+
rule('read', `${P}/(workflow|pipelines(/[^/]+)?)`, 'workflow:read'),
|
|
87
|
+
rule(['PATCH'], `${P}/workflow`, 'workflow:write'),
|
|
88
|
+
rule(['POST'], `${P}/pipelines`, 'workflow:write'),
|
|
89
|
+
rule(['PATCH', 'DELETE'], `${P}/pipelines/[^/]+`, 'workflow:write'),
|
|
90
|
+
// Exports.
|
|
91
|
+
rule('read', `${P}/exports/[^/]+`, 'exports:read'),
|
|
92
|
+
rule(['POST'], `${P}/exports`, 'exports:write'),
|
|
93
|
+
rule(['PATCH', 'DELETE'], `${P}/exports/[^/]+`, 'exports:write'),
|
|
94
|
+
// Jobs: transcoding, rendering, AI, publishing a deliverable.
|
|
95
|
+
rule(['POST'], `${P}/(jobs/[^/]+|pipeline-suggest|deliverables/[^/]+/publish)`, 'jobs:run'),
|
|
96
|
+
];
|
|
97
|
+
/** The rule for a request made with an API token, or undefined: not reachable with one. */
|
|
98
|
+
function routeRuleFor(method, path) {
|
|
99
|
+
const upper = method.toUpperCase();
|
|
100
|
+
const found = RULES.find(r => r.methods.includes(upper) && r.pattern.test(path));
|
|
101
|
+
return found && { permission: found.permission, userTokenOnly: found.userTokenOnly };
|
|
102
|
+
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApiTokenKind, ApiTokenScope, StreamByConfig } from '../types';
|
|
2
|
+
import { ApiTokenPermission } from './apiTokenPermissions';
|
|
2
3
|
export declare const API_TOKEN_PREFIX = "sbt_";
|
|
3
4
|
export declare const API_TOKEN_EXPIRY_DAYS: readonly [7, 30, 90, 365];
|
|
4
5
|
export interface ApiTokenRecord {
|
|
5
6
|
id: string;
|
|
7
|
+
kind: ApiTokenKind;
|
|
6
8
|
userId: string;
|
|
9
|
+
createdByName: string | null;
|
|
7
10
|
name: string;
|
|
8
11
|
prefix: string;
|
|
9
12
|
hash: string;
|
|
10
|
-
|
|
13
|
+
permissions: ApiTokenPermission[];
|
|
11
14
|
projectIds: string[];
|
|
12
15
|
createdAt: Date;
|
|
13
16
|
expiresAt: Date | null;
|
|
14
17
|
lastUsedAt: Date | null;
|
|
15
18
|
}
|
|
16
|
-
/** What a token
|
|
17
|
-
export type ApiTokenInfo = Omit<ApiTokenRecord, 'hash' | 'userId' | 'projectIds'> & {
|
|
19
|
+
/** What's shown of a token: never its hash. */
|
|
20
|
+
export type ApiTokenInfo = Omit<ApiTokenRecord, 'hash' | 'userId' | 'projectIds' | 'createdByName'> & {
|
|
18
21
|
projectIds: string[] | '*';
|
|
22
|
+
createdBy: {
|
|
23
|
+
userId: string;
|
|
24
|
+
username: string | null;
|
|
25
|
+
};
|
|
19
26
|
};
|
|
20
27
|
export declare class ApiTokenError extends Error {
|
|
21
28
|
status: number;
|
|
@@ -23,25 +30,42 @@ export declare class ApiTokenError extends Error {
|
|
|
23
30
|
}
|
|
24
31
|
export interface CreateApiTokenInput {
|
|
25
32
|
name: string;
|
|
26
|
-
|
|
27
|
-
projectIds
|
|
33
|
+
permissions: ApiTokenPermission[];
|
|
34
|
+
projectIds?: string[] | '*';
|
|
28
35
|
expiresInDays: number | null;
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
export interface ApiTokenOwner {
|
|
38
|
+
kind: ApiTokenKind;
|
|
39
|
+
userId: string;
|
|
40
|
+
username?: string | null;
|
|
41
|
+
projectId?: string;
|
|
42
|
+
}
|
|
43
|
+
/** A new token: the token itself, shown only now, and what's kept of it. */
|
|
44
|
+
export declare function createApiToken(owner: ApiTokenOwner, input: CreateApiTokenInput, config: StreamByConfig): Promise<{
|
|
32
45
|
token: string;
|
|
33
46
|
info: ApiTokenInfo;
|
|
34
47
|
}>;
|
|
35
|
-
/**
|
|
36
|
-
export declare function
|
|
37
|
-
/**
|
|
38
|
-
export declare function
|
|
48
|
+
/** A user's own tokens. */
|
|
49
|
+
export declare function listUserApiTokens(userId: string): Promise<ApiTokenInfo[]>;
|
|
50
|
+
/** A project's tokens. */
|
|
51
|
+
export declare function listProjectApiTokens(projectId: string): Promise<ApiTokenInfo[]>;
|
|
52
|
+
/** Revokes a user's own token: it stops working at once. Whether there was one. */
|
|
53
|
+
export declare function revokeUserApiToken(userId: string, tokenId: string): Promise<boolean>;
|
|
54
|
+
/** Revokes a project's token. Whether there was one. */
|
|
55
|
+
export declare function revokeProjectApiToken(projectId: string, tokenId: string): Promise<boolean>;
|
|
56
|
+
/** A project's tokens that `userId` made (to tell its admins they keep working once they leave). */
|
|
57
|
+
export declare function projectApiTokensMadeBy(projectId: string, userId: string): Promise<ApiTokenInfo[]>;
|
|
58
|
+
/** Ends a deleted project's tokens. */
|
|
59
|
+
export declare function deleteProjectApiTokens(projectId: string): Promise<void>;
|
|
60
|
+
/** A project a user's token made: it reaches it from now on (one reaching every project already does). */
|
|
61
|
+
export declare function addProjectToApiToken(tokenId: string, projectId: string): Promise<void>;
|
|
39
62
|
export interface ResolvedApiToken {
|
|
40
63
|
userId: string;
|
|
64
|
+
name: string;
|
|
41
65
|
scope: ApiTokenScope;
|
|
42
66
|
}
|
|
43
67
|
/**
|
|
44
|
-
* The token's
|
|
45
|
-
* known (revoked), or has expired. Marks when it was last used.
|
|
68
|
+
* The token's request: who it is and what it may do, for the host's authProvider; null when
|
|
69
|
+
* it isn't a token, isn't known (revoked), or has expired. Marks when it was last used.
|
|
46
70
|
*/
|
|
47
71
|
export declare function resolveApiToken(token: unknown): Promise<ResolvedApiToken | null>;
|