streamby-core 0.38.0 → 0.40.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 (40) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +108 -1
  3. package/dist/adapters/database/connectionManager.js +55 -0
  4. package/dist/mcp/tools.js +41 -0
  5. package/dist/middleware/createRouter.js +11 -1
  6. package/dist/middleware/routes/ai.js +3 -3
  7. package/dist/middleware/routes/deliverable.d.ts +4 -2
  8. package/dist/middleware/routes/deliverable.js +16 -7
  9. package/dist/middleware/routes/export.js +16 -0
  10. package/dist/middleware/routes/media.js +7 -6
  11. package/dist/middleware/routes/project.js +36 -10
  12. package/dist/middleware/routes/renderFarm.js +4 -77
  13. package/dist/middleware/routes/storage.js +13 -27
  14. package/dist/middleware/routes/worker.d.ts +7 -0
  15. package/dist/middleware/routes/worker.js +125 -0
  16. package/dist/middleware/routes/workerCallback.d.ts +5 -0
  17. package/dist/middleware/routes/workerCallback.js +31 -0
  18. package/dist/services/aiProcessor.js +11 -11
  19. package/dist/services/apiTokenPermissions.js +8 -7
  20. package/dist/services/executor.js +10 -10
  21. package/dist/services/jobQueue.d.ts +6 -4
  22. package/dist/services/jobQueue.js +28 -16
  23. package/dist/services/jobStore.d.ts +27 -0
  24. package/dist/services/jobStore.js +177 -0
  25. package/dist/services/mediaProcessor.js +16 -16
  26. package/dist/services/vfxProcessor.js +12 -12
  27. package/dist/services/workerJobs.d.ts +73 -0
  28. package/dist/services/workerJobs.js +459 -0
  29. package/dist/services/workers.d.ts +78 -0
  30. package/dist/services/workers.js +179 -0
  31. package/dist/types/index.d.ts +52 -2
  32. package/dist/utils/assetCategories.d.ts +4 -0
  33. package/dist/utils/assetCategories.js +21 -0
  34. package/dist/utils/builtinAccess.d.ts +1 -1
  35. package/dist/utils/builtinAccess.js +4 -3
  36. package/dist/utils/privateAddress.d.ts +10 -0
  37. package/dist/utils/privateAddress.js +85 -0
  38. package/dist/utils/sanitize.d.ts +1 -0
  39. package/dist/utils/sanitize.js +14 -0
  40. package/package.json +1 -1
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.renderFarmRouter = renderFarmRouter;
7
4
  const express_1 = require("express");
@@ -9,80 +6,10 @@ const manager_1 = require("../../models/manager");
9
6
  const auth_1 = require("../../utils/auth");
10
7
  const jobQueue_1 = require("../../services/jobQueue");
11
8
  const vfxProcessor_1 = require("../../services/vfxProcessor");
12
- const sanitize_1 = require("../../utils/sanitize");
13
- const crypto_1 = __importDefault(require("crypto"));
14
- const encryption_1 = require("../../utils/encryption");
15
9
  function renderFarmRouter(_config) {
16
10
  const router = (0, express_1.Router)();
17
11
  const Project = (0, manager_1.getModel)('projects');
18
- // ─── Render Farm Connections ─────────────────────────────────────────────────
19
- router.get('/projects/:projectId/render-farm-connections', async (req, res) => {
20
- try {
21
- const auth = req.auth;
22
- const { projectId } = req.params;
23
- const project = await Project.findOne({ _id: projectId });
24
- if (!project)
25
- return res.status(404).json({ message: 'Project not found' });
26
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
27
- return res.status(403).json({ message: 'Unauthorized' });
28
- res.json({ connections: (0, sanitize_1.sanitizeConnections)(project.renderFarmConnections) });
29
- }
30
- catch (err) {
31
- res.status(500).json({ message: err.message });
32
- }
33
- });
34
- router.post('/projects/:projectId/render-farm-connections', async (req, res) => {
35
- try {
36
- const auth = req.auth;
37
- const { projectId } = req.params;
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;
45
- if (!name || !provider || !apiUrl) {
46
- return res.status(400).json({ message: 'name, provider and apiUrl are required' });
47
- }
48
- const project = await Project.findOne({ _id: projectId });
49
- if (!project)
50
- return res.status(404).json({ message: 'Project not found' });
51
- if (!(0, auth_1.canEditProject)(project, auth.userId))
52
- return res.status(403).json({ message: 'Unauthorized' });
53
- const connection = {
54
- id: crypto_1.default.randomUUID(),
55
- name,
56
- provider,
57
- apiUrl,
58
- encryptedCredential,
59
- description,
60
- projectId,
61
- createdAt: new Date(),
62
- };
63
- await Project.update({ _id: projectId }, { $push: { renderFarmConnections: connection } });
64
- res.status(201).json({ connection: (0, sanitize_1.sanitizeConnection)(connection) });
65
- }
66
- catch (err) {
67
- res.status(500).json({ message: err.message });
68
- }
69
- });
70
- router.delete('/projects/:projectId/render-farm-connections/:connectionId', async (req, res) => {
71
- try {
72
- const auth = req.auth;
73
- const { projectId, connectionId } = req.params;
74
- const project = await Project.findOne({ _id: projectId });
75
- if (!project)
76
- return res.status(404).json({ message: 'Project not found' });
77
- if (!(0, auth_1.canEditProject)(project, auth.userId))
78
- return res.status(403).json({ message: 'Unauthorized' });
79
- await Project.update({ _id: projectId }, { $pull: { renderFarmConnections: { id: connectionId } } });
80
- res.json({ message: 'Connection deleted' });
81
- }
82
- catch (err) {
83
- res.status(500).json({ message: err.message });
84
- }
85
- });
12
+ // A project's render farms are its workers: see routes/worker.
86
13
  // ─── VFX Jobs ────────────────────────────────────────────────────────────────
87
14
  router.post('/projects/:projectId/jobs/render', async (req, res) => {
88
15
  try {
@@ -96,7 +23,7 @@ function renderFarmRouter(_config) {
96
23
  return res.status(404).json({ message: 'Project not found' });
97
24
  if (!(0, auth_1.canEditProject)(project, auth.userId))
98
25
  return res.status(403).json({ message: 'Unauthorized' });
99
- const job = (0, jobQueue_1.createJob)('render', auth.userId, projectId, { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat });
26
+ const job = await (0, jobQueue_1.createJob)('render', auth.userId, projectId, { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat });
100
27
  setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, { renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat }));
101
28
  res.status(202).json({ jobId: job.jobId, status: job.status });
102
29
  }
@@ -116,7 +43,7 @@ function renderFarmRouter(_config) {
116
43
  return res.status(404).json({ message: 'Project not found' });
117
44
  if (!(0, auth_1.canEditProject)(project, auth.userId))
118
45
  return res.status(403).json({ message: 'Unauthorized' });
119
- const job = (0, jobQueue_1.createJob)('format-convert', auth.userId, projectId, { fileId, inputFormat, outputFormat, applyTransforms, embedTextures });
46
+ const job = await (0, jobQueue_1.createJob)('format-convert', auth.userId, projectId, { fileId, inputFormat, outputFormat, applyTransforms, embedTextures });
120
47
  setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, { inputFormat, outputFormat, applyTransforms, embedTextures }));
121
48
  res.status(202).json({ jobId: job.jobId, status: job.status });
122
49
  }
@@ -136,7 +63,7 @@ function renderFarmRouter(_config) {
136
63
  return res.status(404).json({ message: 'Project not found' });
137
64
  if (!(0, auth_1.canEditProject)(project, auth.userId))
138
65
  return res.status(403).json({ message: 'Unauthorized' });
139
- const job = (0, jobQueue_1.createJob)('lod', auth.userId, projectId, { fileId, levels, reductionRatios, algorithm, outputFormat });
66
+ const job = await (0, jobQueue_1.createJob)('lod', auth.userId, projectId, { fileId, levels, reductionRatios, algorithm, outputFormat });
140
67
  setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, { levels, reductionRatios, algorithm, outputFormat }));
141
68
  res.status(202).json({ jobId: job.jobId, status: job.status });
142
69
  }
@@ -18,21 +18,7 @@ const crypto_1 = __importDefault(require("crypto"));
18
18
  const signedUrls_1 = require("../../services/signedUrls");
19
19
  const assetVersions_1 = require("../../services/assetVersions");
20
20
  const deliverables_1 = require("../../services/deliverables");
21
- const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
22
- const VALID_3D_EXTENSIONS = new Set(['.glb', '.gltf', '.obj', '.fbx', '.stl', '.ply']);
23
- function validateContentType(contentType, filename, category) {
24
- if (category === 'images')
25
- return contentType.startsWith('image/');
26
- if (category === 'audios')
27
- return contentType.startsWith('audio/');
28
- if (category === 'videos')
29
- return contentType.startsWith('video/');
30
- if (category === '3d-models') {
31
- const ext = filename.split('.').pop()?.toLowerCase();
32
- return ext ? VALID_3D_EXTENSIONS.has(`.${ext}`) : false;
33
- }
34
- return false;
35
- }
21
+ const assetCategories_1 = require("../../utils/assetCategories");
36
22
  function getRawFilesCollection() {
37
23
  const model = (0, manager_1.getModel)('storage_files', 'nosql');
38
24
  const connectionIds = model.getConnectionIds();
@@ -113,13 +99,13 @@ function storageRouter(config) {
113
99
  const auth = req.auth;
114
100
  const { projectId, category } = req.params;
115
101
  const { fileName, contentType } = req.query;
116
- if (!VALID_CATEGORIES.includes(category)) {
117
- return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
102
+ if (!assetCategories_1.VALID_CATEGORIES.includes(category)) {
103
+ return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
118
104
  }
119
105
  if (!fileName || !contentType) {
120
106
  return res.status(400).json({ message: 'Query params fileName and contentType are required' });
121
107
  }
122
- if (!validateContentType(contentType, fileName, category)) {
108
+ if (!(0, assetCategories_1.validateContentType)(contentType, fileName, category)) {
123
109
  const hint = category === '3d-models'
124
110
  ? '.glb, .gltf, .obj, .fbx, .stl, or .ply'
125
111
  : `${category.slice(0, -1)}/* content type`;
@@ -179,7 +165,7 @@ function storageRouter(config) {
179
165
  const fileDoc = await collection.findOne({ fileId, projectId });
180
166
  if (!fileDoc)
181
167
  return res.status(404).json({ message: 'File not found' });
182
- if (!validateContentType(contentType, fileName, fileDoc.category)) {
168
+ if (!(0, assetCategories_1.validateContentType)(contentType, fileName, fileDoc.category)) {
183
169
  return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
184
170
  }
185
171
  if (!adapter.getPresignedUploadUrl) {
@@ -277,8 +263,8 @@ function storageRouter(config) {
277
263
  try {
278
264
  const auth = req.auth;
279
265
  const { projectId, category } = req.params;
280
- if (!VALID_CATEGORIES.includes(category)) {
281
- return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
266
+ if (!assetCategories_1.VALID_CATEGORIES.includes(category)) {
267
+ return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
282
268
  }
283
269
  const project = await Project.findOne({ _id: projectId });
284
270
  if (!project)
@@ -350,13 +336,13 @@ function storageRouter(config) {
350
336
  const auth = req.auth;
351
337
  const { projectId, connId } = req.params;
352
338
  const { fileName, contentType, category } = req.query;
353
- if (!category || !VALID_CATEGORIES.includes(category)) {
354
- return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
339
+ if (!category || !assetCategories_1.VALID_CATEGORIES.includes(category)) {
340
+ return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
355
341
  }
356
342
  if (!fileName || !contentType) {
357
343
  return res.status(400).json({ message: 'Query params fileName and contentType are required' });
358
344
  }
359
- if (!validateContentType(contentType, fileName, category)) {
345
+ if (!(0, assetCategories_1.validateContentType)(contentType, fileName, category)) {
360
346
  return res.status(400).json({ message: `File does not match category ${category}` });
361
347
  }
362
348
  const project = await Project.findOne({ _id: projectId });
@@ -424,8 +410,8 @@ function storageRouter(config) {
424
410
  try {
425
411
  const auth = req.auth;
426
412
  const { projectId, connId, category } = req.params;
427
- if (!VALID_CATEGORIES.includes(category)) {
428
- return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
413
+ if (!assetCategories_1.VALID_CATEGORIES.includes(category)) {
414
+ return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
429
415
  }
430
416
  const project = await Project.findOne({ _id: projectId });
431
417
  if (!project)
@@ -465,7 +451,7 @@ function storageRouter(config) {
465
451
  const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
466
452
  if (!fileDoc)
467
453
  return res.status(404).json({ message: 'File not found' });
468
- if (!validateContentType(contentType, fileName, fileDoc.category)) {
454
+ if (!(0, assetCategories_1.validateContentType)(contentType, fileName, fileDoc.category)) {
469
455
  return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
470
456
  }
471
457
  const connAdapter = await resolveConnAdapter(connId, project, auth);
@@ -0,0 +1,7 @@
1
+ import { Router, Request } from 'express';
2
+ import { StorageAdapter, StreamByConfig } from '../../types';
3
+ /** Where workers call back to: this router, as the request reached it (unless the host says). */
4
+ export declare const callbackBaseOf: (req: Request, config: StreamByConfig) => string;
5
+ export declare function workerRouter(config: StreamByConfig & {
6
+ adapter?: StorageAdapter;
7
+ }): Router;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callbackBaseOf = void 0;
4
+ exports.workerRouter = workerRouter;
5
+ const express_1 = require("express");
6
+ const manager_1 = require("../../models/manager");
7
+ const auth_1 = require("../../utils/auth");
8
+ const jobQueue_1 = require("../../services/jobQueue");
9
+ const workers_1 = require("../../services/workers");
10
+ const workerJobs_1 = require("../../services/workerJobs");
11
+ /** Where workers call back to: this router, as the request reached it (unless the host says). */
12
+ const callbackBaseOf = (req, config) => config.workerOptions?.callbackBaseUrl ?? `${req.protocol}://${req.get('host')}${req.baseUrl}`;
13
+ exports.callbackBaseOf = callbackBaseOf;
14
+ // A project's processing workers (its own and the host's), and its jobs: asking a worker to
15
+ // process an asset, following them, and retrying a failed one.
16
+ function workerRouter(config) {
17
+ const router = (0, express_1.Router)();
18
+ const Project = (0, manager_1.getModel)('projects');
19
+ const failed = (res, what, err) => {
20
+ if (err instanceof workers_1.WorkerError)
21
+ return res.status(err.status).json({ message: err.message });
22
+ res.status(500).json({ message: `Failed to ${what}`, details: err?.message });
23
+ };
24
+ const projectFor = async (req, res, edit) => {
25
+ const auth = req.auth;
26
+ const project = await Project.findOne({ _id: req.params.projectId });
27
+ if (!project) {
28
+ res.status(404).json({ message: 'Project not found' });
29
+ return null;
30
+ }
31
+ if (!(edit ? auth_1.canEditProject : auth_1.isProjectMember)(project, auth.userId)) {
32
+ res.status(403).json({ message: 'Unauthorized' });
33
+ return null;
34
+ }
35
+ return project;
36
+ };
37
+ // ── Workers ──
38
+ router.get('/projects/:projectId/workers', async (req, res) => {
39
+ try {
40
+ const project = await projectFor(req, res, false);
41
+ if (!project)
42
+ return;
43
+ res.json({ data: await (0, workers_1.availableWorkers)(project, req.auth, config) });
44
+ }
45
+ catch (err) {
46
+ failed(res, 'list workers', err);
47
+ }
48
+ });
49
+ router.post('/projects/:projectId/workers', async (req, res) => {
50
+ try {
51
+ const auth = req.auth;
52
+ const project = await projectFor(req, res, true);
53
+ if (!project)
54
+ return;
55
+ const worker = (0, workers_1.newWorkerConnection)(req.body ?? {}, req.params.projectId, auth.userId, config);
56
+ await (0, workers_1.assertPublicEndpoint)(worker.apiUrl, config);
57
+ await Project.update({ _id: req.params.projectId }, { $push: { renderFarmConnections: worker } });
58
+ res.status(201).json({ data: (0, workers_1.summarizeProjectWorker)(worker) });
59
+ }
60
+ catch (err) {
61
+ failed(res, 'connect the worker', err);
62
+ }
63
+ });
64
+ router.delete('/projects/:projectId/workers/:workerId', async (req, res) => {
65
+ try {
66
+ const project = await projectFor(req, res, true);
67
+ if (!project)
68
+ return;
69
+ if (!(0, workers_1.projectWorkers)(project).some(w => w.id === req.params.workerId))
70
+ return res.status(404).json({ message: 'Worker not found' });
71
+ await Project.update({ _id: req.params.projectId }, { $pull: { renderFarmConnections: { id: req.params.workerId } } });
72
+ res.json({ message: 'Worker removed' });
73
+ }
74
+ catch (err) {
75
+ failed(res, 'remove the worker', err);
76
+ }
77
+ });
78
+ // ── Jobs ──
79
+ router.get('/projects/:projectId/jobs', async (req, res) => {
80
+ try {
81
+ const project = await projectFor(req, res, false);
82
+ if (!project)
83
+ return;
84
+ const assetId = typeof req.query.assetId === 'string' ? req.query.assetId : undefined;
85
+ res.json({ data: (await (0, jobQueue_1.listJobs)(req.params.projectId, assetId)).map(workerJobs_1.publicJob) });
86
+ }
87
+ catch (err) {
88
+ failed(res, 'list jobs', err);
89
+ }
90
+ });
91
+ // A worker processes an asset (or one of its versions): { assetId, versionId?, worker:
92
+ // { source, id }, kind, profile?, resultMode?: 'derived' | 'version' }.
93
+ router.post('/projects/:projectId/jobs/process', async (req, res) => {
94
+ try {
95
+ const project = await projectFor(req, res, true);
96
+ if (!project)
97
+ return;
98
+ const job = await (0, workerJobs_1.enqueueWorkerJob)(project, req.body ?? {}, req.auth, config, (0, exports.callbackBaseOf)(req, config));
99
+ res.status(202).json({ job: (0, workerJobs_1.publicJob)(job) });
100
+ }
101
+ catch (err) {
102
+ failed(res, 'start the job', err);
103
+ }
104
+ });
105
+ router.post('/projects/:projectId/jobs/:jobId/retry', async (req, res) => {
106
+ try {
107
+ const project = await projectFor(req, res, true);
108
+ if (!project)
109
+ return;
110
+ const job = await (0, jobQueue_1.getJob)(req.params.jobId);
111
+ if (!job || job.projectId !== req.params.projectId)
112
+ return res.status(404).json({ message: 'Job not found' });
113
+ if (!job.worker)
114
+ return res.status(400).json({ message: 'Only a worker job can be retried' });
115
+ const retried = await (0, workerJobs_1.retryWorkerJob)(job.jobId, config);
116
+ if (!retried)
117
+ return res.status(409).json({ message: `Only a failed job can be retried; this one is ${job.status}` });
118
+ res.status(202).json({ job: (0, workerJobs_1.publicJob)(retried) });
119
+ }
120
+ catch (err) {
121
+ failed(res, 'retry the job', err);
122
+ }
123
+ });
124
+ return router;
125
+ }
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import { StorageAdapter, StreamByConfig } from '../../types';
3
+ export declare function workerCallbackRouter(config: StreamByConfig & {
4
+ adapter?: StorageAdapter;
5
+ }): Router;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workerCallbackRouter = workerCallbackRouter;
4
+ const express_1 = require("express");
5
+ const workers_1 = require("../../services/workers");
6
+ const workerJobs_1 = require("../../services/workerJobs");
7
+ // A worker calling back about the job it was sent: with no session, only the job's own token
8
+ // (`Authorization: Bearer <callback.token>`), which works for that attempt alone.
9
+ function workerCallbackRouter(config) {
10
+ const router = (0, express_1.Router)();
11
+ const handle = (action) => async (req, res) => {
12
+ try {
13
+ const job = await (0, workerJobs_1.authorizeCallback)(req.params.jobId, req.headers.authorization);
14
+ res.json(await action(job, req));
15
+ }
16
+ catch (err) {
17
+ if (err instanceof workers_1.WorkerError)
18
+ return res.status(err.status).json({ message: err.message });
19
+ console.error('❌ StreamBy workers: callback failed:', err?.message ?? err);
20
+ res.status(500).json({ message: 'Callback failed' });
21
+ }
22
+ };
23
+ router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/progress`, handle(async (job, req) => ({ job: (0, workerJobs_1.publicJob)((await (0, workerJobs_1.reportProgress)(job, req.body)) ?? job) })));
24
+ router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/upload-url`, handle((job, req) => (0, workerJobs_1.resultUploadUrl)(job, req.body, config)));
25
+ router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/complete`, handle(async (job, req) => ({ job: (0, workerJobs_1.publicJob)(await (0, workerJobs_1.completeJob)(job, req.body, config)) })));
26
+ router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/fail`, handle(async (job, req) => {
27
+ const after = await (0, workerJobs_1.failWorkerJob)(job, req.body, config);
28
+ return { job: after ? (0, workerJobs_1.publicJob)(after) : null };
29
+ }));
30
+ return router;
31
+ }
@@ -13,7 +13,7 @@ function getNosqlDb() {
13
13
  return (0, connectionManager_1.getConnection)(ids[0]).client.db();
14
14
  }
15
15
  async function runTranscriptionJob(jobId, fileId, projectId, options) {
16
- (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
16
+ await (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
17
17
  try {
18
18
  const db = getNosqlDb();
19
19
  if (!db)
@@ -24,7 +24,7 @@ async function runTranscriptionJob(jobId, fileId, projectId, options) {
24
24
  if (!fileDoc.contentType?.startsWith('video/') && !fileDoc.contentType?.startsWith('audio/')) {
25
25
  throw new Error('Transcription only supports video and audio files');
26
26
  }
27
- (0, jobQueue_1.updateJob)(jobId, { stage: 'queued-provider', progress: 20 });
27
+ await (0, jobQueue_1.updateJob)(jobId, { stage: 'queued-provider', progress: 20 });
28
28
  await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
29
29
  $set: {
30
30
  assetId: fileId,
@@ -41,14 +41,14 @@ async function runTranscriptionJob(jobId, fileId, projectId, options) {
41
41
  extractedAt: new Date(),
42
42
  },
43
43
  }, { upsert: true });
44
- (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
44
+ await (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
45
45
  }
46
46
  catch (err) {
47
- (0, jobQueue_1.failJob)(jobId, err.message);
47
+ await (0, jobQueue_1.failJob)(jobId, err.message);
48
48
  }
49
49
  }
50
50
  async function runUpscaleJob(jobId, fileId, projectId, options) {
51
- (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
51
+ await (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
52
52
  try {
53
53
  const db = getNosqlDb();
54
54
  if (!db)
@@ -56,7 +56,7 @@ async function runUpscaleJob(jobId, fileId, projectId, options) {
56
56
  const fileDoc = await db.collection('storage_files').findOne({ fileId, projectId });
57
57
  if (!fileDoc)
58
58
  throw new Error(`File ${fileId} not found`);
59
- (0, jobQueue_1.updateJob)(jobId, { stage: 'queued-upscale', progress: 20 });
59
+ await (0, jobQueue_1.updateJob)(jobId, { stage: 'queued-upscale', progress: 20 });
60
60
  await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
61
61
  $set: {
62
62
  assetId: fileId,
@@ -72,14 +72,14 @@ async function runUpscaleJob(jobId, fileId, projectId, options) {
72
72
  extractedAt: new Date(),
73
73
  },
74
74
  }, { upsert: true });
75
- (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
75
+ await (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
76
76
  }
77
77
  catch (err) {
78
- (0, jobQueue_1.failJob)(jobId, err.message);
78
+ await (0, jobQueue_1.failJob)(jobId, err.message);
79
79
  }
80
80
  }
81
81
  async function runGenerateAssetJob(jobId, projectId, options) {
82
- (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'queued-generation', progress: 10 });
82
+ await (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'queued-generation', progress: 10 });
83
83
  try {
84
84
  const db = getNosqlDb();
85
85
  if (!db)
@@ -95,10 +95,10 @@ async function runGenerateAssetJob(jobId, projectId, options) {
95
95
  status: 'pending',
96
96
  requestedAt: new Date(),
97
97
  });
98
- (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 20 });
98
+ await (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 20 });
99
99
  }
100
100
  catch (err) {
101
- (0, jobQueue_1.failJob)(jobId, err.message);
101
+ await (0, jobQueue_1.failJob)(jobId, err.message);
102
102
  }
103
103
  }
104
104
  async function buildPipelineSuggestion(nodeSchema, availableNodeTypes) {
@@ -39,7 +39,7 @@ const RULES = [
39
39
  rule('read', '^/users/search', 'members:manage'),
40
40
  rule('read', '^/jobs/[^/]+', 'jobs:read'),
41
41
  // The project itself and its settings.
42
- rule('read', `${P}(/preview)?`, 'project:read'),
42
+ rule('read', `${P}(/preview|/origins|/integrations)?`, 'project:read'),
43
43
  rule(['PATCH'], P, 'project:update'),
44
44
  rule('delete', P, 'project:delete'),
45
45
  rule(['PATCH'], `${P}/(archive|unarchive)`, 'project:update', { userTokenOnly: true }),
@@ -74,10 +74,10 @@ const RULES = [
74
74
  // Rights.
75
75
  rule('read', `${P}/assets/[^/]+/rights`, 'rights:read'),
76
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'),
77
+ // Connections (API, database, storage, distribution, workers): the project's settings.
78
+ rule('read', `${P}/(connections|get-connection|distribution-connections|workers)(/.+)?`, 'project:read'),
79
+ rule('write', `${P}/(connections|distribution-connections|workers)(/.+)?`, 'project:update'),
80
+ rule('delete', `${P}/(connections|distribution-connections|workers)/.+`, 'project:update'),
81
81
  // Production.
82
82
  rule('read', `${P}/production/.+`, 'production:read'),
83
83
  rule('write', `${P}/production/.+`, 'production:write'),
@@ -88,11 +88,12 @@ const RULES = [
88
88
  rule(['POST'], `${P}/pipelines`, 'workflow:write'),
89
89
  rule(['PATCH', 'DELETE'], `${P}/pipelines/[^/]+`, 'workflow:write'),
90
90
  // Exports.
91
- rule('read', `${P}/exports/[^/]+`, 'exports:read'),
91
+ rule('read', `${P}/exports(/[^/]+)?`, 'exports:read'),
92
92
  rule(['POST'], `${P}/exports`, 'exports:write'),
93
93
  rule(['PATCH', 'DELETE'], `${P}/exports/[^/]+`, 'exports:write'),
94
94
  // Jobs: transcoding, rendering, AI, publishing a deliverable.
95
- rule(['POST'], `${P}/(jobs/[^/]+|pipeline-suggest|deliverables/[^/]+/publish)`, 'jobs:run'),
95
+ rule('read', `${P}/jobs`, 'jobs:read'),
96
+ rule(['POST'], `${P}/(jobs/[^/]+(/retry)?|pipeline-suggest|deliverables/[^/]+/publish)`, 'jobs:run'),
96
97
  ];
97
98
  /** The rule for a request made with an API token, or undefined: not reachable with one. */
98
99
  function routeRuleFor(method, path) {
@@ -216,7 +216,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
216
216
  if (isPublic && JOB_NODES.has(processNode.type))
217
217
  throw notPublic(processNode);
218
218
  if (processNode.type === 'transcodeNode' && fileId) {
219
- const job = (0, jobQueue_1.createJob)('transcode', systemUserId, projectId, nodeData);
219
+ const job = await (0, jobQueue_1.createJob)('transcode', systemUserId, projectId, nodeData);
220
220
  setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, {
221
221
  codec: nodeData.codec,
222
222
  resolution: nodeData.resolution,
@@ -227,7 +227,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
227
227
  payload = { ...payload, jobId: job.jobId, jobType: 'transcode' };
228
228
  }
229
229
  else if (processNode.type === 'captionNode' && fileId) {
230
- const job = (0, jobQueue_1.createJob)('caption', systemUserId, projectId, nodeData);
230
+ const job = await (0, jobQueue_1.createJob)('caption', systemUserId, projectId, nodeData);
231
231
  setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, {
232
232
  sourceLanguage: nodeData.sourceLanguage,
233
233
  outputFormat: nodeData.outputFormat,
@@ -236,7 +236,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
236
236
  payload = { ...payload, jobId: job.jobId, jobType: 'caption' };
237
237
  }
238
238
  else if (processNode.type === 'thumbnailNode' && fileId) {
239
- const job = (0, jobQueue_1.createJob)('thumbnail', systemUserId, projectId, nodeData);
239
+ const job = await (0, jobQueue_1.createJob)('thumbnail', systemUserId, projectId, nodeData);
240
240
  setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, {
241
241
  timecode: nodeData.timecode,
242
242
  resolution: nodeData.resolution,
@@ -256,12 +256,12 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
256
256
  if ('error' in storageResolved) {
257
257
  throw new NodeExecutionError(processNode.id, processNode.type, `Node '${processNode.id}' (${processNode.type}): ${storageResolved.error}`);
258
258
  }
259
- const job = (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
259
+ const job = await (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
260
260
  setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
261
261
  payload = { ...payload, jobId: job.jobId, jobType: 'ingest' };
262
262
  }
263
263
  else if (processNode.type === 'renderJobNode' && fileId) {
264
- const job = (0, jobQueue_1.createJob)('render', systemUserId, projectId, nodeData);
264
+ const job = await (0, jobQueue_1.createJob)('render', systemUserId, projectId, nodeData);
265
265
  setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, {
266
266
  renderer: nodeData.renderer,
267
267
  renderFarmConnectionId: nodeData.renderFarmConnectionId,
@@ -273,7 +273,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
273
273
  payload = { ...payload, jobId: job.jobId, jobType: 'render' };
274
274
  }
275
275
  else if (processNode.type === 'formatConvertNode' && fileId) {
276
- const job = (0, jobQueue_1.createJob)('format-convert', systemUserId, projectId, nodeData);
276
+ const job = await (0, jobQueue_1.createJob)('format-convert', systemUserId, projectId, nodeData);
277
277
  setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, {
278
278
  inputFormat: nodeData.inputFormat,
279
279
  outputFormat: nodeData.outputFormat,
@@ -283,7 +283,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
283
283
  payload = { ...payload, jobId: job.jobId, jobType: 'format-convert' };
284
284
  }
285
285
  else if (processNode.type === 'lodNode' && fileId) {
286
- const job = (0, jobQueue_1.createJob)('lod', systemUserId, projectId, nodeData);
286
+ const job = await (0, jobQueue_1.createJob)('lod', systemUserId, projectId, nodeData);
287
287
  setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, {
288
288
  levels: nodeData.levels,
289
289
  reductionRatios: nodeData.reductionRatios,
@@ -300,7 +300,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
300
300
  payload = { ...payload, qcReport: { checks: checkResults, overallPassed: true } };
301
301
  }
302
302
  else if (processNode.type === 'transcriptionNode' && fileId) {
303
- const job = (0, jobQueue_1.createJob)('transcription', systemUserId, projectId, nodeData);
303
+ const job = await (0, jobQueue_1.createJob)('transcription', systemUserId, projectId, nodeData);
304
304
  setImmediate(() => (0, aiProcessor_1.runTranscriptionJob)(job.jobId, fileId, projectId, {
305
305
  model: nodeData.model,
306
306
  sourceLanguage: nodeData.sourceLanguage,
@@ -311,7 +311,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
311
311
  payload = { ...payload, jobId: job.jobId, jobType: 'transcription' };
312
312
  }
313
313
  else if (processNode.type === 'upscaleNode' && fileId) {
314
- const job = (0, jobQueue_1.createJob)('upscale', systemUserId, projectId, nodeData);
314
+ const job = await (0, jobQueue_1.createJob)('upscale', systemUserId, projectId, nodeData);
315
315
  setImmediate(() => (0, aiProcessor_1.runUpscaleJob)(job.jobId, fileId, projectId, {
316
316
  scale: nodeData.scale,
317
317
  model: nodeData.model,
@@ -321,7 +321,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
321
321
  payload = { ...payload, jobId: job.jobId, jobType: 'upscale' };
322
322
  }
323
323
  else if (processNode.type === 'proceduralAssetNode') {
324
- const job = (0, jobQueue_1.createJob)('generate-asset', systemUserId, projectId, nodeData);
324
+ const job = await (0, jobQueue_1.createJob)('generate-asset', systemUserId, projectId, nodeData);
325
325
  setImmediate(() => (0, aiProcessor_1.runGenerateAssetJob)(job.jobId, projectId, {
326
326
  assetType: nodeData.assetType,
327
327
  provider: nodeData.provider,
@@ -1,5 +1,7 @@
1
1
  import { JobRecord, JobType } from '../types';
2
- export declare function createJob(jobType: JobType, userId: string, projectId: string, payload: Record<string, any>): JobRecord;
3
- export declare function getJob(jobId: string): JobRecord | undefined;
4
- export declare function updateJob(jobId: string, updates: Partial<Pick<JobRecord, 'status' | 'stage' | 'progress' | 'assetId' | 'error'>>): void;
5
- export declare function failJob(jobId: string, error: string): void;
2
+ export declare function createJob(jobType: JobType, userId: string, projectId: string, payload: Record<string, any>, extra?: Partial<JobRecord>): Promise<JobRecord>;
3
+ export declare function getJob(jobId: string): Promise<JobRecord | null>;
4
+ export declare function listJobs(projectId: string, assetId?: string): Promise<JobRecord[]>;
5
+ export declare function notify(job: JobRecord): void;
6
+ export declare function updateJob(jobId: string, updates: Partial<JobRecord>): Promise<JobRecord | null>;
7
+ export declare function failJob(jobId: string, error: string): Promise<JobRecord | null>;