streamby-core 0.33.0 → 0.34.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +661 -0
  3. package/README.md +53 -2
  4. package/dist/middleware/createRouter.js +15 -2
  5. package/dist/middleware/routes/ai.js +3 -3
  6. package/dist/middleware/routes/connection.js +3 -12
  7. package/dist/middleware/routes/dbConnection.js +7 -26
  8. package/dist/middleware/routes/distribution.js +12 -5
  9. package/dist/middleware/routes/export.js +5 -4
  10. package/dist/middleware/routes/media.js +23 -10
  11. package/dist/middleware/routes/member.js +3 -12
  12. package/dist/middleware/routes/pipeline.js +3 -3
  13. package/dist/middleware/routes/production.js +9 -9
  14. package/dist/middleware/routes/project.js +8 -25
  15. package/dist/middleware/routes/renderFarm.js +13 -6
  16. package/dist/middleware/routes/review.js +4 -4
  17. package/dist/middleware/routes/rights.js +1 -1
  18. package/dist/middleware/routes/storage.js +21 -15
  19. package/dist/middleware/routes/storageConnection.js +2 -8
  20. package/dist/middleware/routes/workflow.js +1 -1
  21. package/dist/services/auth.d.ts +1 -2
  22. package/dist/services/auth.js +9 -11
  23. package/dist/services/connectionResolver.js +2 -2
  24. package/dist/services/executor.d.ts +4 -1
  25. package/dist/services/executor.js +29 -11
  26. package/dist/services/reencryptCredentials.d.ts +5 -0
  27. package/dist/services/reencryptCredentials.js +74 -0
  28. package/dist/services/userIntegration.d.ts +2 -2
  29. package/dist/services/userIntegration.js +7 -1
  30. package/dist/types/index.d.ts +2 -0
  31. package/dist/utils/auth.d.ts +15 -1
  32. package/dist/utils/auth.js +21 -6
  33. package/dist/utils/encryption.d.ts +6 -1
  34. package/dist/utils/encryption.js +80 -27
  35. package/dist/utils/serviceEndpoints.d.ts +2 -0
  36. package/dist/utils/serviceEndpoints.js +26 -0
  37. package/package.json +4 -3
  38. package/dist/middleware/routes/apiConnection.d.ts +0 -3
  39. package/dist/middleware/routes/apiConnection.js +0 -100
  40. package/dist/middleware/routes/credential.d.ts +0 -3
  41. package/dist/middleware/routes/credential.js +0 -80
  42. package/dist/services/apiConnection.d.ts +0 -18
  43. package/dist/services/apiConnection.js +0 -65
  44. package/dist/services/credential.d.ts +0 -8
  45. package/dist/services/credential.js +0 -70
  46. package/dist/services/pipeline.d.ts +0 -2
  47. package/dist/services/pipeline.js +0 -292
@@ -72,9 +72,6 @@ function projectRouter(config) {
72
72
  if (req.subscription === 'freemium') {
73
73
  return res.status(403).json({ error: 'Freemium users cannot create projects' });
74
74
  }
75
- if (auth.role !== 'admin' && auth.role !== 'editor') {
76
- return res.status(403).json({ message: 'Permission denied' });
77
- }
78
75
  const { name, description, image, allowedOrigin, public: isPublic = true, category, integrationIds } = req.body;
79
76
  if (isPublic === false && req.subscription === 'freemium') {
80
77
  return res.status(403).json({ error: 'Private projects require a subscriber plan' });
@@ -208,9 +205,6 @@ function projectRouter(config) {
208
205
  router.patch('/projects/:id', async (req, res) => {
209
206
  try {
210
207
  const auth = req.auth;
211
- if (auth.role !== 'admin' && auth.role !== 'editor') {
212
- return res.status(403).json({ message: 'Permission denied' });
213
- }
214
208
  const projectId = req.params.id;
215
209
  const updates = req.body;
216
210
  const project = await Project.findOne({ _id: projectId });
@@ -220,7 +214,7 @@ function projectRouter(config) {
220
214
  if (!updates || typeof updates !== 'object') {
221
215
  return res.status(400).json({ message: 'Missing updates payload' });
222
216
  }
223
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
217
+ if (!(0, auth_1.canEditProject)(project, auth.userId)) {
224
218
  return res.status(403).json({ message: 'Unauthorized project access' });
225
219
  }
226
220
  const updated = await Project.update({ _id: projectId }, updates);
@@ -250,8 +244,9 @@ function projectRouter(config) {
250
244
  if (!project) {
251
245
  return res.status(404).json({ message: 'Project not found' });
252
246
  }
253
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
254
- return res.status(403).json({ message: 'Unauthorized project access' });
247
+ // Only an admin of the project deletes it.
248
+ if (!(0, auth_1.isProjectAdmin)(project, auth.userId)) {
249
+ return res.status(403).json({ message: 'Only a project admin can delete it' });
255
250
  }
256
251
  await Project.useDbType(project.dbType).delete({ _id: projectId });
257
252
  const allProjects = await Project.find({});
@@ -348,9 +343,6 @@ function projectRouter(config) {
348
343
  router.post('/projects/:id/image', async (req, res) => {
349
344
  try {
350
345
  const auth = req.auth;
351
- if (auth.role !== 'admin' && auth.role !== 'editor') {
352
- return res.status(403).json({ message: 'Permission denied' });
353
- }
354
346
  const projectId = req.params.id;
355
347
  const { imageUrl } = req.body;
356
348
  if (!projectId || !imageUrl) {
@@ -360,7 +352,7 @@ function projectRouter(config) {
360
352
  if (!project) {
361
353
  return res.status(404).json({ message: 'Project not found' });
362
354
  }
363
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
355
+ if (!(0, auth_1.canEditProject)(project, auth.userId)) {
364
356
  return res.status(403).json({ message: 'Unauthorized project access' });
365
357
  }
366
358
  const updatedProject = await Project.update({ _id: projectId }, { image: imageUrl });
@@ -376,16 +368,13 @@ function projectRouter(config) {
376
368
  router.patch('/projects/:id/origins', async (req, res) => {
377
369
  try {
378
370
  const auth = req.auth;
379
- if (auth.role !== 'admin' && auth.role !== 'editor') {
380
- return res.status(403).json({ message: 'Permission denied' });
381
- }
382
371
  const projectId = req.params.id;
383
372
  const { origins } = req.body;
384
373
  const project = await Project.findOne({ _id: projectId });
385
374
  if (!project) {
386
375
  return res.status(404).json({ message: 'Project not found' });
387
376
  }
388
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
377
+ if (!(0, auth_1.canEditProject)(project, auth.userId)) {
389
378
  return res.status(403).json({ message: 'Unauthorized project access' });
390
379
  }
391
380
  const updated = await Project.update({ _id: projectId }, { allowedOrigin: origins });
@@ -402,9 +391,6 @@ function projectRouter(config) {
402
391
  router.post('/projects/:id/integrations', async (req, res) => {
403
392
  try {
404
393
  const auth = req.auth;
405
- if (auth.role !== 'admin' && auth.role !== 'editor') {
406
- return res.status(403).json({ message: 'Permission denied' });
407
- }
408
394
  const projectId = req.params.id;
409
395
  const { integrationId } = req.body;
410
396
  if (!integrationId)
@@ -412,7 +398,7 @@ function projectRouter(config) {
412
398
  const project = await Project.findOne({ _id: projectId });
413
399
  if (!project)
414
400
  return res.status(404).json({ message: 'Project not found' });
415
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
401
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
416
402
  return res.status(403).json({ message: 'Unauthorized project access' });
417
403
  const alreadyConnected = [...(project.dbConnections ?? []), ...(project.storageConnections ?? []), ...(project.integrationConnections ?? [])]
418
404
  .some((c) => c.integrationId === integrationId);
@@ -438,15 +424,12 @@ function projectRouter(config) {
438
424
  router.delete('/projects/:id/integrations/:integrationId', async (req, res) => {
439
425
  try {
440
426
  const auth = req.auth;
441
- if (auth.role !== 'admin' && auth.role !== 'editor') {
442
- return res.status(403).json({ message: 'Permission denied' });
443
- }
444
427
  const projectId = req.params.id;
445
428
  const { integrationId } = req.params;
446
429
  const project = await Project.findOne({ _id: projectId });
447
430
  if (!project)
448
431
  return res.status(404).json({ message: 'Project not found' });
449
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
432
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
450
433
  return res.status(403).json({ message: 'Unauthorized project access' });
451
434
  const dbConn = (project.dbConnections ?? []).find((c) => c.integrationId === integrationId);
452
435
  const storageConn = (project.storageConnections ?? []).find((c) => c.integrationId === integrationId);
@@ -11,6 +11,7 @@ const jobQueue_1 = require("../../services/jobQueue");
11
11
  const vfxProcessor_1 = require("../../services/vfxProcessor");
12
12
  const sanitize_1 = require("../../utils/sanitize");
13
13
  const crypto_1 = __importDefault(require("crypto"));
14
+ const encryption_1 = require("../../utils/encryption");
14
15
  function renderFarmRouter(_config) {
15
16
  const router = (0, express_1.Router)();
16
17
  const Project = (0, manager_1.getModel)('projects');
@@ -34,14 +35,20 @@ function renderFarmRouter(_config) {
34
35
  try {
35
36
  const auth = req.auth;
36
37
  const { projectId } = req.params;
37
- const { name, provider, apiUrl, encryptedCredential, description } = req.body;
38
+ const { name, provider, apiUrl, credential: credentialValue, encryptedCredential: legacyCredential, description } = req.body;
39
+ // The credential, as given (`encryptedCredential` was its old name: it was always the
40
+ // value itself), stored encrypted.
41
+ const credential = credentialValue ?? legacyCredential;
42
+ if (credential && !(0, encryption_1.isEncryptionKeySet)())
43
+ return res.status(500).json({ message: 'Encryption key is not set' });
44
+ const encryptedCredential = credential ? (0, encryption_1.encrypt)(String(credential)) : undefined;
38
45
  if (!name || !provider || !apiUrl) {
39
46
  return res.status(400).json({ message: 'name, provider and apiUrl are required' });
40
47
  }
41
48
  const project = await Project.findOne({ _id: projectId });
42
49
  if (!project)
43
50
  return res.status(404).json({ message: 'Project not found' });
44
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
51
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
45
52
  return res.status(403).json({ message: 'Unauthorized' });
46
53
  const connection = {
47
54
  id: crypto_1.default.randomUUID(),
@@ -67,7 +74,7 @@ function renderFarmRouter(_config) {
67
74
  const project = await Project.findOne({ _id: projectId });
68
75
  if (!project)
69
76
  return res.status(404).json({ message: 'Project not found' });
70
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
77
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
71
78
  return res.status(403).json({ message: 'Unauthorized' });
72
79
  await Project.update({ _id: projectId }, { $pull: { renderFarmConnections: { id: connectionId } } });
73
80
  res.json({ message: 'Connection deleted' });
@@ -87,7 +94,7 @@ function renderFarmRouter(_config) {
87
94
  const project = await Project.findOne({ _id: projectId });
88
95
  if (!project)
89
96
  return res.status(404).json({ message: 'Project not found' });
90
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
97
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
91
98
  return res.status(403).json({ message: 'Unauthorized' });
92
99
  const job = (0, jobQueue_1.createJob)('render', auth.userId, projectId, { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat });
93
100
  setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, { renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat }));
@@ -107,7 +114,7 @@ function renderFarmRouter(_config) {
107
114
  const project = await Project.findOne({ _id: projectId });
108
115
  if (!project)
109
116
  return res.status(404).json({ message: 'Project not found' });
110
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
117
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
111
118
  return res.status(403).json({ message: 'Unauthorized' });
112
119
  const job = (0, jobQueue_1.createJob)('format-convert', auth.userId, projectId, { fileId, inputFormat, outputFormat, applyTransforms, embedTextures });
113
120
  setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, { inputFormat, outputFormat, applyTransforms, embedTextures }));
@@ -127,7 +134,7 @@ function renderFarmRouter(_config) {
127
134
  const project = await Project.findOne({ _id: projectId });
128
135
  if (!project)
129
136
  return res.status(404).json({ message: 'Project not found' });
130
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
137
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
131
138
  return res.status(403).json({ message: 'Unauthorized' });
132
139
  const job = (0, jobQueue_1.createJob)('lod', auth.userId, projectId, { fileId, levels, reductionRatios, algorithm, outputFormat });
133
140
  setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, { levels, reductionRatios, algorithm, outputFormat }));
@@ -60,7 +60,7 @@ function reviewRouter(_config) {
60
60
  const project = await Project.findOne({ _id: projectId });
61
61
  if (!project)
62
62
  return res.status(404).json({ message: 'Project not found' });
63
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
63
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
64
64
  return res.status(403).json({ message: 'Unauthorized' });
65
65
  const db = getDb();
66
66
  if (!db)
@@ -120,7 +120,7 @@ function reviewRouter(_config) {
120
120
  const project = await Project.findOne({ _id: projectId });
121
121
  if (!project)
122
122
  return res.status(404).json({ message: 'Project not found' });
123
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
123
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
124
124
  return res.status(403).json({ message: 'Unauthorized' });
125
125
  const db = getDb();
126
126
  if (!db)
@@ -190,7 +190,7 @@ function reviewRouter(_config) {
190
190
  const project = await Project.findOne({ _id: projectId });
191
191
  if (!project)
192
192
  return res.status(404).json({ message: 'Project not found' });
193
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
193
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
194
194
  return res.status(403).json({ message: 'Unauthorized' });
195
195
  const db = getDb();
196
196
  if (!db)
@@ -224,7 +224,7 @@ function reviewRouter(_config) {
224
224
  const project = await Project.findOne({ _id: projectId });
225
225
  if (!project)
226
226
  return res.status(404).json({ message: 'Project not found' });
227
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
227
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
228
228
  return res.status(403).json({ message: 'Unauthorized' });
229
229
  const db = getDb();
230
230
  if (!db)
@@ -42,7 +42,7 @@ function rightsRouter(_config) {
42
42
  const project = await Project.findOne({ _id: projectId });
43
43
  if (!project)
44
44
  return res.status(404).json({ message: 'Project not found' });
45
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
45
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
46
46
  return res.status(403).json({ message: 'Unauthorized' });
47
47
  const db = getDb();
48
48
  if (!db)
@@ -71,11 +71,14 @@ function storageRouter(config) {
71
71
  router.get('/upload-project-image-url/:id', async (req, res) => {
72
72
  try {
73
73
  const auth = req.auth;
74
- if (auth.role !== 'admin' && auth.role !== 'editor')
75
- return res.status(403).json({ message: 'Permission denied' });
76
74
  const projectId = req.params.id;
77
75
  if (!projectId)
78
76
  return res.status(400).json({ message: 'Missing projectId' });
77
+ const project = await Project.findOne({ _id: projectId });
78
+ if (!project)
79
+ return res.status(404).json({ message: 'Project not found' });
80
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
81
+ return res.status(403).json({ message: 'Permission denied' });
79
82
  const response = await (0, presign_1.getPresignedProjectImageUrl)(adapter, projectId);
80
83
  res.status(200).json({ ...response, message: 'Presigned URL generated successfully' });
81
84
  }
@@ -86,11 +89,14 @@ function storageRouter(config) {
86
89
  router.delete('/delete-project-image-url/:id', async (req, res) => {
87
90
  try {
88
91
  const auth = req.auth;
89
- if (auth.role !== 'admin' && auth.role !== 'editor')
90
- return res.status(403).json({ message: 'Permission denied' });
91
92
  const projectId = req.params.id;
92
93
  if (!projectId)
93
94
  return res.status(400).json({ message: 'Missing projectId' });
95
+ const project = await Project.findOne({ _id: projectId });
96
+ if (!project)
97
+ return res.status(404).json({ message: 'Project not found' });
98
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
99
+ return res.status(403).json({ message: 'Permission denied' });
94
100
  await (0, file_1.deleteProjectImage)(adapter, projectId);
95
101
  const updated = await Project.update({ _id: projectId }, { image: '' });
96
102
  res.status(201).json({ ...updated, message: 'Image deleted successfully' });
@@ -119,7 +125,7 @@ function storageRouter(config) {
119
125
  const project = await Project.findOne({ _id: projectId });
120
126
  if (!project)
121
127
  return res.status(404).json({ message: 'Project not found' });
122
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
128
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
123
129
  return res.status(403).json({ message: 'Unauthorized access' });
124
130
  if (!adapter.getPresignedUploadUrl) {
125
131
  return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
@@ -192,7 +198,7 @@ function storageRouter(config) {
192
198
  const project = await Project.findOne({ _id: projectId });
193
199
  if (!project)
194
200
  return res.status(404).json({ message: 'Project not found' });
195
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
201
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
196
202
  return res.status(403).json({ message: 'Unauthorized access' });
197
203
  const collection = getRawFilesCollection();
198
204
  if (!collection)
@@ -213,7 +219,7 @@ function storageRouter(config) {
213
219
  const project = await Project.findOne({ _id: projectId });
214
220
  if (!project)
215
221
  return res.status(404).json({ message: 'Project not found' });
216
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
222
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
217
223
  return res.status(403).json({ message: 'Unauthorized access' });
218
224
  const collection = getRawFilesCollection();
219
225
  if (!collection)
@@ -291,7 +297,7 @@ function storageRouter(config) {
291
297
  const project = await Project.findOne({ _id: projectId });
292
298
  if (!project)
293
299
  return res.status(404).json({ message: 'Project not found' });
294
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
300
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
295
301
  return res.status(403).json({ message: 'Unauthorized access' });
296
302
  const connAdapter = await resolveConnAdapter(connId, project, auth);
297
303
  if ('error' in connAdapter)
@@ -419,7 +425,7 @@ function storageRouter(config) {
419
425
  const project = await Project.findOne({ _id: projectId });
420
426
  if (!project)
421
427
  return res.status(404).json({ message: 'Project not found' });
422
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
428
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
423
429
  return res.status(403).json({ message: 'Unauthorized access' });
424
430
  const collection = getRawFilesCollection();
425
431
  if (!collection)
@@ -440,7 +446,7 @@ function storageRouter(config) {
440
446
  const project = await Project.findOne({ _id: projectId });
441
447
  if (!project)
442
448
  return res.status(404).json({ message: 'Project not found' });
443
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
449
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
444
450
  return res.status(403).json({ message: 'Unauthorized access' });
445
451
  const connAdapter = await resolveConnAdapter(connId, project, auth);
446
452
  if ('error' in connAdapter)
@@ -539,7 +545,7 @@ function storageRouter(config) {
539
545
  const project = await Project.findOne({ _id: projectId });
540
546
  if (!project)
541
547
  return res.status(404).json({ message: 'Project not found' });
542
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
548
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
543
549
  return res.status(403).json({ message: 'Unauthorized access' });
544
550
  const conn = findStorageConn(project, connId);
545
551
  const isBuiltin = conn?.source === 'builtin';
@@ -590,7 +596,7 @@ function storageRouter(config) {
590
596
  const project = await Project.findOne({ _id: projectId });
591
597
  if (!project)
592
598
  return res.status(404).json({ message: 'Project not found' });
593
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
599
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
594
600
  return res.status(403).json({ message: 'Unauthorized access' });
595
601
  const collection = getRawFoldersCollection();
596
602
  if (!collection)
@@ -624,7 +630,7 @@ function storageRouter(config) {
624
630
  const project = await Project.findOne({ _id: projectId });
625
631
  if (!project)
626
632
  return res.status(404).json({ message: 'Project not found' });
627
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
633
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
628
634
  return res.status(403).json({ message: 'Unauthorized access' });
629
635
  const collection = getRawFoldersCollection();
630
636
  if (!collection)
@@ -654,7 +660,7 @@ function storageRouter(config) {
654
660
  const project = await Project.findOne({ _id: projectId });
655
661
  if (!project)
656
662
  return res.status(404).json({ message: 'Project not found' });
657
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
663
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
658
664
  return res.status(403).json({ message: 'Unauthorized access' });
659
665
  const foldersCollection = getRawFoldersCollection();
660
666
  if (!foldersCollection)
@@ -682,7 +688,7 @@ function storageRouter(config) {
682
688
  const project = await Project.findOne({ _id: projectId });
683
689
  if (!project)
684
690
  return res.status(404).json({ message: 'Project not found' });
685
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
691
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
686
692
  return res.status(403).json({ message: 'Unauthorized access' });
687
693
  if (folderId != null) {
688
694
  const foldersCollection = getRawFoldersCollection();
@@ -40,9 +40,6 @@ function storageConnectionRouter(config) {
40
40
  router.post('/projects/:id/connections/storage', async (req, res) => {
41
41
  try {
42
42
  const auth = req.auth;
43
- if (auth.role !== 'admin' && auth.role !== 'editor') {
44
- return res.status(403).json({ message: 'Permission denied' });
45
- }
46
43
  const { name, type, credential, integrationId, description } = req.body;
47
44
  if (!name || !type) {
48
45
  return res.status(400).json({ message: 'name and type are required' });
@@ -59,7 +56,7 @@ function storageConnectionRouter(config) {
59
56
  const project = await Project.findOne({ _id: req.params.id });
60
57
  if (!project)
61
58
  return res.status(404).json({ message: 'Project not found' });
62
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
59
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
63
60
  return res.status(403).json({ message: 'Unauthorized project access' });
64
61
  let encryptedCredential;
65
62
  if (integrationId) {
@@ -98,13 +95,10 @@ function storageConnectionRouter(config) {
98
95
  try {
99
96
  const { connId } = req.params;
100
97
  const auth = req.auth;
101
- if (auth.role !== 'admin' && auth.role !== 'editor') {
102
- return res.status(403).json({ message: 'Permission denied' });
103
- }
104
98
  const project = await Project.findOne({ _id: req.params.id });
105
99
  if (!project)
106
100
  return res.status(404).json({ message: 'Project not found' });
107
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
101
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
108
102
  return res.status(403).json({ message: 'Unauthorized project access' });
109
103
  const conn = project.storageConnections?.find((c) => c.id === connId);
110
104
  if (!conn)
@@ -38,7 +38,7 @@ function workflowRouter(config) {
38
38
  return res.status(401).json({ message: 'Unauthorized' });
39
39
  const projectId = req.params.id;
40
40
  const project = await Project.findOne({ _id: projectId });
41
- if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
41
+ if (!project || !(0, auth_1.canEditProject)(project, auth.userId)) {
42
42
  return res.status(403).json({ message: 'Unauthorized project access' });
43
43
  }
44
44
  const now = new Date();
@@ -1,5 +1,4 @@
1
- import { Auth } from '../types';
2
1
  import { Request, Response, NextFunction } from 'express';
3
2
  import { StreamByConfig } from '../types';
3
+ export declare const isPublicRoute: (method: string, path: string) => boolean;
4
4
  export declare const authenticate: (config: StreamByConfig) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
5
- export declare function checkRole(auth: Auth, required: 'viewer' | 'editor' | 'admin'): void;
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.authenticate = void 0;
4
- exports.checkRole = checkRole;
3
+ exports.authenticate = exports.isPublicRoute = void 0;
4
+ // The one route open without a session: a project's public export, read
5
+ // (GET /:projectId/export/:exportName, see routes/export). Exactly that path -- not any path
6
+ // that has an "export" segment somewhere.
7
+ const PUBLIC_EXPORT = /^\/[^/]+\/export\/[^/]+\/?$/;
8
+ const READ_METHODS = ['GET', 'HEAD', 'OPTIONS'];
9
+ const isPublicRoute = (method, path) => READ_METHODS.includes(method.toUpperCase()) && PUBLIC_EXPORT.test(path);
10
+ exports.isPublicRoute = isPublicRoute;
5
11
  const authenticate = (config) => async (req, res, next) => {
6
- if (req.path.includes('/export/')) {
12
+ if ((0, exports.isPublicRoute)(req.method, req.path)) {
7
13
  return next();
8
14
  }
9
15
  try {
@@ -20,11 +26,3 @@ const authenticate = (config) => async (req, res, next) => {
20
26
  }
21
27
  };
22
28
  exports.authenticate = authenticate;
23
- function checkRole(auth, required) {
24
- const roles = ['viewer', 'editor', 'admin'];
25
- const userIdx = roles.indexOf(auth.role);
26
- const requiredIdx = roles.indexOf(required);
27
- if (userIdx < requiredIdx) {
28
- throw new Error(`Insufficient role: requires ${required}, found ${auth.role}`);
29
- }
30
- }
@@ -52,7 +52,7 @@ async function resolveDbConnectionClient(project, connId, config, auth) {
52
52
  if (conn.source === 'integration') {
53
53
  if (!conn.integrationId)
54
54
  return { error: 'Connection is missing its integrationId', status: 500 };
55
- const resolved = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId, config);
55
+ const resolved = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId, config, project);
56
56
  if (!resolved)
57
57
  return { error: 'Integration not found', status: 400 };
58
58
  connectionString = resolved.credential;
@@ -109,7 +109,7 @@ async function resolveStorageAdapter(project, connId, config, auth) {
109
109
  if (!conn.integrationId)
110
110
  return { error: 'Connection is missing its integrationId', status: 500 };
111
111
  try {
112
- const resolved = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId, config);
112
+ const resolved = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId, config, project);
113
113
  if (!resolved)
114
114
  return { error: 'Integration not found', status: 400 };
115
115
  return new s3_1.S3Adapter(resolved.credential);
@@ -4,4 +4,7 @@ export declare class NodeExecutionError extends Error {
4
4
  nodeType: string;
5
5
  constructor(nodeId: string, nodeType: string, message: string);
6
6
  }
7
- export declare function executeExport(nodeSchema: NodeSchema, project: any, config: StreamByConfig): Promise<any>;
7
+ export interface ExecuteExportOptions {
8
+ public?: boolean;
9
+ }
10
+ export declare function executeExport(nodeSchema: NodeSchema, project: any, config: StreamByConfig, { public: isPublic }?: ExecuteExportOptions): Promise<any>;
@@ -12,6 +12,7 @@ const mediaProcessor_1 = require("./mediaProcessor");
12
12
  const vfxProcessor_1 = require("./vfxProcessor");
13
13
  const aiProcessor_1 = require("./aiProcessor");
14
14
  const storage_1 = require("../providers/storage");
15
+ const serviceEndpoints_1 = require("../utils/serviceEndpoints");
15
16
  function applyFilterConfig(payload, config) {
16
17
  const isArr = Array.isArray(payload);
17
18
  let r = isArr ? [...payload] : payload;
@@ -87,13 +88,19 @@ class NodeExecutionError extends Error {
87
88
  }
88
89
  }
89
90
  exports.NodeExecutionError = NodeExecutionError;
90
- // Export execution is public (gated by allowedOrigin, not user identity — see
91
- // export.ts:339) — there is no real Auth to pass through. This sentinel satisfies
92
- // resolveDbConnectionClient's signature without adding a gate that never existed;
93
- // canUseBuiltin gating is for connection *management* by a user, not for serving an
94
- // already-published export.
91
+ // Export execution has no user behind it (a public export is read without a session -- see
92
+ // routes/export) -- there is no real Auth to pass through. This sentinel satisfies
93
+ // resolveDbConnectionClient's signature; canUseBuiltin gating is for connection *management*
94
+ // by a user, not for serving an already-published export.
95
95
  const SYSTEM_AUTH = { userId: 'system', username: 'system', role: 'admin' };
96
- async function executeExport(nodeSchema, project, config) {
96
+ // Nodes that start a job (transcoding, rendering, generating...): work, and cost, on the
97
+ // server or a provider, for whoever asks.
98
+ const JOB_NODES = new Set([
99
+ 'transcodeNode', 'captionNode', 'thumbnailNode', 'ingestNode', 'renderJobNode', 'formatConvertNode',
100
+ 'lodNode', 'transcriptionNode', 'upscaleNode', 'proceduralAssetNode',
101
+ ]);
102
+ const notPublic = (node) => new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}) can't run in a public export.`);
103
+ async function executeExport(nodeSchema, project, config, { public: isPublic = true } = {}) {
97
104
  const nodes = nodeSchema.nodes;
98
105
  const edges = nodeSchema.edges;
99
106
  // 1. Data layer — nodes connected to streamby in-bottom
@@ -119,6 +126,8 @@ async function executeExport(nodeSchema, project, config) {
119
126
  if (!connectionId) {
120
127
  throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}) has no database connection — connect one on the node.`);
121
128
  }
129
+ if (isPublic && (0, connectionResolver_1.findDbConnection)(project, connectionId)?.source === 'integration')
130
+ throw notPublic(node);
122
131
  const recordId = node.data?.recordId;
123
132
  const projectIdentifier = project._id?.toString() ?? project.id;
124
133
  const resolved = await (0, connectionResolver_1.resolveDbConnectionClient)(project, connectionId, config, SYSTEM_AUTH);
@@ -158,6 +167,8 @@ async function executeExport(nodeSchema, project, config) {
158
167
  // NodeExecutionError (matching dataSourceNode/ingestNode's intentional pattern), since
159
168
  // a silently-skipped service action would produce a workflow that "succeeds" while
160
169
  // quietly missing the one thing the node exists to do.
170
+ if (isPublic)
171
+ throw notPublic(node);
161
172
  const connectionId = node.data?.connectionId;
162
173
  if (!connectionId) {
163
174
  throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}) has no integration connection — connect one on the node.`);
@@ -166,21 +177,26 @@ async function executeExport(nodeSchema, project, config) {
166
177
  if (!connection) {
167
178
  throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): integration connection not found on this project.`);
168
179
  }
169
- const resolved = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(connection.integrationId, config);
170
- if (!resolved) {
171
- throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): integration credential could not be resolved — it may have been disconnected.`);
172
- }
173
180
  const endpoint = node.data?.endpoint;
174
181
  if (!endpoint) {
175
182
  throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}) has no endpoint configured.`);
176
183
  }
184
+ // Checked before the credential is even resolved: it's only ever sent to its provider.
185
+ if (!(0, serviceEndpoints_1.isServiceEndpoint)(connection.provider, endpoint)) {
186
+ throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): ${endpoint} is not a ${connection.provider} endpoint.`);
187
+ }
188
+ const resolved = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(connection.integrationId, config, project);
189
+ if (!resolved) {
190
+ throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): integration credential could not be resolved — it may have been disconnected.`);
191
+ }
177
192
  const method = node.data?.method ?? 'GET';
178
193
  const headers = {
179
194
  'Content-Type': 'application/json',
180
195
  ...(0, oauthClient_1.buildServiceAuthHeaders)(connection.provider, resolved.credential),
181
196
  };
182
197
  const body = node.data?.body ? JSON.stringify(node.data.body) : undefined;
183
- const response = await fetch(endpoint, { method, headers, body });
198
+ // A redirect would carry the credential's headers on to wherever it points.
199
+ const response = await fetch(endpoint, { method, headers, body, redirect: 'error' });
184
200
  if (!response.ok) {
185
201
  throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): integration action failed (${response.statusText}).`);
186
202
  }
@@ -197,6 +213,8 @@ async function executeExport(nodeSchema, project, config) {
197
213
  while (processNode) {
198
214
  const nodeData = processNode.data ?? {};
199
215
  const fileId = nodeData.fileId;
216
+ if (isPublic && JOB_NODES.has(processNode.type))
217
+ throw notPublic(processNode);
200
218
  if (processNode.type === 'transcodeNode' && fileId) {
201
219
  const job = (0, jobQueue_1.createJob)('transcode', systemUserId, projectId, nodeData);
202
220
  setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, {
@@ -0,0 +1,5 @@
1
+ export interface ReencryptResult {
2
+ updated: number;
3
+ failed: number;
4
+ }
5
+ export declare function reencryptStoredCredentials(): Promise<ReencryptResult>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reencryptStoredCredentials = reencryptStoredCredentials;
4
+ const manager_1 = require("../models/manager");
5
+ const encryption_1 = require("../utils/encryption");
6
+ // Brings every stored credential to the current encryption (AES-256-GCM with the current key,
7
+ // see utils/encryption): those encrypted before (CBC, or GCM with a previous key) are
8
+ // re-encrypted, and the distribution and render farm credentials, stored as they were sent,
9
+ // are encrypted. Non-destructive: a value is replaced only by its own re-encryption; one that
10
+ // can't be decrypted is left as it is (and counted). Runs at startup; once everything is
11
+ // current, it changes nothing.
12
+ // A project's lists of connections with a credential each, encrypted before.
13
+ const ENCRYPTED_LISTS = ['connections', 'dbConnections', 'storageConnections', 'exports'];
14
+ // The ones whose credentials were stored as sent, never encrypted.
15
+ const PLAIN_LISTS = ['distributionConnections', 'renderFarmConnections'];
16
+ const INTEGRATION_FIELDS = ['encryptedCredential', 'encryptedAccessToken', 'encryptedRefreshToken'];
17
+ async function reencryptStoredCredentials() {
18
+ const result = { updated: 0, failed: 0 };
19
+ if (!(0, encryption_1.isEncryptionKeySet)())
20
+ return result;
21
+ // The current encryption of a stored value, or undefined when it's current already or
22
+ // can't be read (left as it is).
23
+ const upgrade = (value, storedPlain) => {
24
+ if (typeof value !== 'string' || !value || (0, encryption_1.isCurrentEncryption)(value))
25
+ return undefined;
26
+ if (storedPlain && !(0, encryption_1.isVersionedEncryption)(value))
27
+ return (0, encryption_1.encrypt)(value);
28
+ try {
29
+ return (0, encryption_1.encrypt)((0, encryption_1.decrypt)(value));
30
+ }
31
+ catch {
32
+ result.failed++;
33
+ return undefined;
34
+ }
35
+ };
36
+ const Project = (0, manager_1.getModel)('projects');
37
+ for (const project of (await Project.find({}))) {
38
+ const changes = {};
39
+ for (const list of [...ENCRYPTED_LISTS, ...PLAIN_LISTS]) {
40
+ const entries = project[list];
41
+ if (!Array.isArray(entries))
42
+ continue;
43
+ const storedPlain = PLAIN_LISTS.includes(list);
44
+ let changed = false;
45
+ const next = entries.map((entry) => {
46
+ const upgraded = upgrade(entry?.encryptedCredential, storedPlain);
47
+ if (upgraded === undefined)
48
+ return entry;
49
+ changed = true;
50
+ return { ...entry, encryptedCredential: upgraded };
51
+ });
52
+ if (changed)
53
+ changes[list] = next;
54
+ }
55
+ if (Object.keys(changes).length) {
56
+ await Project.update({ _id: project._id ?? project.id }, changes);
57
+ result.updated++;
58
+ }
59
+ }
60
+ const UserIntegration = (0, manager_1.getModel)('user_integrations');
61
+ for (const integration of (await UserIntegration.find({}))) {
62
+ const changes = {};
63
+ for (const field of INTEGRATION_FIELDS) {
64
+ const upgraded = upgrade(integration[field], false);
65
+ if (upgraded !== undefined)
66
+ changes[field] = upgraded;
67
+ }
68
+ if (Object.keys(changes).length) {
69
+ await UserIntegration.update({ id: integration.id }, changes);
70
+ result.updated++;
71
+ }
72
+ }
73
+ return result;
74
+ }