streamby-core 0.39.0 → 0.41.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 (63) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +661 -661
  3. package/README.md +785 -573
  4. package/dist/adapters/database/connectionManager.js +182 -124
  5. package/dist/adapters/database/sql.js +1 -1
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.js +9 -1
  8. package/dist/mcp/audit.d.ts +5 -2
  9. package/dist/mcp/audit.js +8 -0
  10. package/dist/mcp/server.js +3 -3
  11. package/dist/mcp/tools.js +41 -0
  12. package/dist/middleware/createRouter.js +14 -1
  13. package/dist/middleware/routes/ai.js +3 -3
  14. package/dist/middleware/routes/deliverable.d.ts +4 -2
  15. package/dist/middleware/routes/deliverable.js +16 -7
  16. package/dist/middleware/routes/mcp.js +11 -4
  17. package/dist/middleware/routes/media.js +7 -6
  18. package/dist/middleware/routes/{apiConnection.d.ts → oauthResource.d.ts} +1 -1
  19. package/dist/middleware/routes/oauthResource.js +18 -0
  20. package/dist/middleware/routes/project.js +5 -1
  21. package/dist/middleware/routes/renderFarm.js +4 -77
  22. package/dist/middleware/routes/storage.js +13 -27
  23. package/dist/middleware/routes/worker.d.ts +7 -0
  24. package/dist/middleware/routes/worker.js +125 -0
  25. package/dist/middleware/routes/workerCallback.d.ts +5 -0
  26. package/dist/middleware/routes/workerCallback.js +31 -0
  27. package/dist/models/Model.js +9 -9
  28. package/dist/services/aiProcessor.js +11 -11
  29. package/dist/services/apiTokenPermissions.js +6 -5
  30. package/dist/services/auth.d.ts +2 -0
  31. package/dist/services/auth.js +36 -10
  32. package/dist/services/executor.js +10 -10
  33. package/dist/services/jobQueue.d.ts +6 -4
  34. package/dist/services/jobQueue.js +28 -16
  35. package/dist/services/jobStore.d.ts +27 -0
  36. package/dist/services/jobStore.js +177 -0
  37. package/dist/services/mediaProcessor.js +16 -16
  38. package/dist/services/oauthTokens.d.ts +60 -0
  39. package/dist/services/oauthTokens.js +164 -0
  40. package/dist/services/vfxProcessor.js +12 -12
  41. package/dist/services/workerJobs.d.ts +73 -0
  42. package/dist/services/workerJobs.js +459 -0
  43. package/dist/services/workers.d.ts +78 -0
  44. package/dist/services/workers.js +179 -0
  45. package/dist/types/index.d.ts +64 -2
  46. package/dist/utils/assetCategories.d.ts +4 -0
  47. package/dist/utils/assetCategories.js +21 -0
  48. package/dist/utils/builtinAccess.d.ts +1 -1
  49. package/dist/utils/builtinAccess.js +4 -3
  50. package/dist/utils/privateAddress.d.ts +10 -0
  51. package/dist/utils/privateAddress.js +85 -0
  52. package/package.json +1 -1
  53. package/dist/middleware/routes/apiConnection.js +0 -100
  54. package/dist/middleware/routes/credential.d.ts +0 -3
  55. package/dist/middleware/routes/credential.js +0 -80
  56. package/dist/services/apiConnection.d.ts +0 -18
  57. package/dist/services/apiConnection.js +0 -65
  58. package/dist/services/credential.d.ts +0 -8
  59. package/dist/services/credential.js +0 -70
  60. package/dist/services/pipeline.d.ts +0 -2
  61. package/dist/services/pipeline.js +0 -292
  62. package/dist/utils/crypto.d.ts +0 -3
  63. package/dist/utils/crypto.js +0 -32
@@ -2,15 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deliverableRouter = deliverableRouter;
4
4
  const express_1 = require("express");
5
+ const workerJobs_1 = require("../../services/workerJobs");
6
+ const worker_1 = require("./worker");
7
+ const auth_1 = require("../../services/auth");
5
8
  const manager_1 = require("../../models/manager");
6
- const auth_1 = require("../../utils/auth");
9
+ const auth_2 = require("../../utils/auth");
7
10
  const nosqlDb_1 = require("../../utils/nosqlDb");
8
11
  const assetVersions_1 = require("../../services/assetVersions");
9
12
  const deliverables_1 = require("../../services/deliverables");
10
13
  const MAX_NOTE = 1000;
11
14
  // A project's deliverables (see services/deliverables): marked, listed and removed. Removing
12
- // one never deletes its asset.
13
- function deliverableRouter(_config) {
15
+ // one never deletes its asset. Marking one starts the workers set to process its kind of asset
16
+ // by themselves.
17
+ function deliverableRouter(config) {
14
18
  const router = (0, express_1.Router)();
15
19
  const Project = (0, manager_1.getModel)('projects');
16
20
  const exportsOf = (project) => project.exports ?? [];
@@ -48,7 +52,7 @@ function deliverableRouter(_config) {
48
52
  const project = await Project.findOne({ _id: projectId });
49
53
  if (!project)
50
54
  return res.status(404).json({ message: 'Project not found' });
51
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
55
+ if (!(0, auth_2.isProjectMember)(project, auth.userId))
52
56
  return res.status(403).json({ message: 'Unauthorized' });
53
57
  const deliverables = await (0, deliverables_1.listDeliverables)(projectId, { shotId, exportId });
54
58
  res.json({ deliverables: await describe(project, deliverables) });
@@ -77,7 +81,7 @@ function deliverableRouter(_config) {
77
81
  const project = await Project.findOne({ _id: projectId });
78
82
  if (!project)
79
83
  return res.status(404).json({ message: 'Project not found' });
80
- if (!(0, auth_1.canEditProject)(project, auth.userId))
84
+ if (!(0, auth_2.canEditProject)(project, auth.userId))
81
85
  return res.status(403).json({ message: 'Unauthorized' });
82
86
  const db = (0, nosqlDb_1.nosqlDb)();
83
87
  if (!db)
@@ -104,7 +108,12 @@ function deliverableRouter(_config) {
104
108
  createdBy: auth.userId,
105
109
  });
106
110
  const [described] = await describe(project, [deliverable]);
107
- res.status(201).json({ deliverable: described });
111
+ // The project's workers set to process its kind of asset by themselves start on it: not
112
+ // for an API token that can't run jobs.
113
+ const jobs = (0, auth_1.tokenAllows)(auth.apiToken, 'jobs:run')
114
+ ? await (0, workerJobs_1.enqueueAutomaticJobs)(project, assetId, versionId || null, auth, config, (0, worker_1.callbackBaseOf)(req, config)).catch(() => [])
115
+ : [];
116
+ res.status(201).json({ deliverable: described, jobs: jobs.map(workerJobs_1.publicJob) });
108
117
  }
109
118
  catch (err) {
110
119
  res.status(500).json({ message: 'Failed to mark the deliverable', details: err.message });
@@ -117,7 +126,7 @@ function deliverableRouter(_config) {
117
126
  const project = await Project.findOne({ _id: projectId });
118
127
  if (!project)
119
128
  return res.status(404).json({ message: 'Project not found' });
120
- if (!(0, auth_1.canEditProject)(project, auth.userId))
129
+ if (!(0, auth_2.canEditProject)(project, auth.userId))
121
130
  return res.status(403).json({ message: 'Unauthorized' });
122
131
  if (!(await (0, deliverables_1.deleteDeliverable)(projectId, deliverableId)))
123
132
  return res.status(404).json({ message: 'Deliverable not found' });
@@ -5,11 +5,13 @@ const express_1 = require("express");
5
5
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
6
6
  const server_1 = require("../../mcp/server");
7
7
  const rateLimit_1 = require("../../mcp/rateLimit");
8
+ const oauthTokens_1 = require("../../services/oauthTokens");
8
9
  // eslint-disable-next-line @typescript-eslint/no-var-requires
9
10
  const { version } = require('../../../package.json');
10
11
  const jsonRpcError = (res, status, message) => res.status(status).json({ jsonrpc: '2.0', error: { code: -32000, message }, id: null });
11
12
  // StreamBy's MCP server (Streamable HTTP, stateless: one server per request), for agents
12
- // working with StreamBy through an API token. Each tool calls the REST API right here -- this
13
+ // working with StreamBy through an API token, or an OAuth access token of an app the user
14
+ // signed in to (e.g. claude.ai's connector). Each tool calls the REST API right here -- this
13
15
  // same process, under the same mount -- with the request's own Authorization header, so it
14
16
  // reaches what the token and its owner can and nothing more.
15
17
  function mcpRouter(config) {
@@ -17,12 +19,17 @@ function mcpRouter(config) {
17
19
  const limiter = (0, rateLimit_1.createRateLimiter)(config.mcp?.rateLimit ?? rateLimit_1.DEFAULT_MCP_RATE_LIMIT);
18
20
  router.post('/mcp', async (req, res) => {
19
21
  const auth = req.auth;
20
- if (!auth.apiToken)
21
- return jsonRpcError(res, 401, 'The MCP server takes an API token (Authorization: Bearer sbt_...)');
22
+ // A browser session isn't for agents: they come with a token.
23
+ if (!auth.apiToken) {
24
+ if (config.oauth)
25
+ res.setHeader('WWW-Authenticate', (0, oauthTokens_1.wwwAuthenticate)(config.oauth, false));
26
+ return jsonRpcError(res, 401, 'The MCP server takes an API token (Authorization: Bearer sbt_...) or an OAuth access token');
27
+ }
28
+ // Per token; an app is counted per user it acts for.
22
29
  const taken = limiter.take(auth.apiToken.tokenId);
23
30
  if (!taken.allowed) {
24
31
  res.setHeader('Retry-After', String(Math.ceil(taken.retryAfterMs / 1000)));
25
- return jsonRpcError(res, 429, 'Too many requests for this API token; try again later');
32
+ return jsonRpcError(res, 429, 'Too many requests for this token; try again later');
26
33
  }
27
34
  const base = config.mcp?.apiBaseUrl ?? `http://127.0.0.1:${req.socket.localPort}${req.baseUrl}`;
28
35
  const api = async ({ method, path, body }) => {
@@ -8,6 +8,7 @@ const jobQueue_1 = require("../../services/jobQueue");
8
8
  const mediaProcessor_1 = require("../../services/mediaProcessor");
9
9
  const storage_1 = require("../../providers/storage");
10
10
  const connectionManager_1 = require("../../adapters/database/connectionManager");
11
+ const workerJobs_1 = require("../../services/workerJobs");
11
12
  const auth_2 = require("../../services/auth");
12
13
  function getMetadataCollection() {
13
14
  const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
@@ -31,7 +32,7 @@ function mediaRouter(config) {
31
32
  router.get('/jobs/:jobId', async (req, res) => {
32
33
  try {
33
34
  const auth = req.auth;
34
- const job = (0, jobQueue_1.getJob)(req.params.jobId);
35
+ const job = await (0, jobQueue_1.getJob)(req.params.jobId);
35
36
  if (!job)
36
37
  return res.status(404).json({ message: 'Job not found' });
37
38
  // With an API token, only a job of a project it reaches.
@@ -42,7 +43,7 @@ function mediaRouter(config) {
42
43
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
43
44
  return res.status(404).json({ message: 'Job not found' });
44
45
  }
45
- res.json({ job });
46
+ res.json({ job: (0, workerJobs_1.publicJob)(job) });
46
47
  }
47
48
  catch (err) {
48
49
  res.status(500).json({ message: 'Failed to fetch job', details: err.message });
@@ -61,7 +62,7 @@ function mediaRouter(config) {
61
62
  return res.status(404).json({ message: 'Project not found' });
62
63
  if (!(0, auth_1.canEditProject)(project, auth.userId))
63
64
  return res.status(403).json({ message: 'Unauthorized' });
64
- const job = (0, jobQueue_1.createJob)('ingest', auth.userId, projectId, { fileId });
65
+ const job = await (0, jobQueue_1.createJob)('ingest', auth.userId, projectId, { fileId });
65
66
  // Run async — response returns immediately with jobId
66
67
  setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
67
68
  res.status(202).json({ jobId: job.jobId, status: job.status });
@@ -83,7 +84,7 @@ function mediaRouter(config) {
83
84
  return res.status(404).json({ message: 'Project not found' });
84
85
  if (!(0, auth_1.canEditProject)(project, auth.userId))
85
86
  return res.status(403).json({ message: 'Unauthorized' });
86
- const job = (0, jobQueue_1.createJob)('transcode', auth.userId, projectId, {
87
+ const job = await (0, jobQueue_1.createJob)('transcode', auth.userId, projectId, {
87
88
  fileId, codec, resolution, outputFormat, bitrate, audioCodec,
88
89
  });
89
90
  setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, { codec, resolution, outputFormat, bitrate, audioCodec }, adapter));
@@ -106,7 +107,7 @@ function mediaRouter(config) {
106
107
  return res.status(404).json({ message: 'Project not found' });
107
108
  if (!(0, auth_1.canEditProject)(project, auth.userId))
108
109
  return res.status(403).json({ message: 'Unauthorized' });
109
- const job = (0, jobQueue_1.createJob)('thumbnail', auth.userId, projectId, { fileId, timecode, resolution, strategy });
110
+ const job = await (0, jobQueue_1.createJob)('thumbnail', auth.userId, projectId, { fileId, timecode, resolution, strategy });
110
111
  setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, { timecode, resolution, strategy }));
111
112
  res.status(202).json({ jobId: job.jobId, status: job.status });
112
113
  }
@@ -127,7 +128,7 @@ function mediaRouter(config) {
127
128
  return res.status(404).json({ message: 'Project not found' });
128
129
  if (!(0, auth_1.canEditProject)(project, auth.userId))
129
130
  return res.status(403).json({ message: 'Unauthorized' });
130
- const job = (0, jobQueue_1.createJob)('caption', auth.userId, projectId, { fileId, sourceLanguage, outputFormat, provider });
131
+ const job = await (0, jobQueue_1.createJob)('caption', auth.userId, projectId, { fileId, sourceLanguage, outputFormat, provider });
131
132
  setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, { sourceLanguage, outputFormat, provider }));
132
133
  res.status(202).json({ jobId: job.jobId, status: job.status });
133
134
  }
@@ -1,3 +1,3 @@
1
1
  import { Router } from 'express';
2
2
  import { StreamByConfig } from '../../types';
3
- export declare function apiConnectionRouter(config: StreamByConfig): Router;
3
+ export declare function oauthResourceRouter(config: StreamByConfig): Router;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oauthResourceRouter = oauthResourceRouter;
4
+ const express_1 = require("express");
5
+ const oauthTokens_1 = require("../../services/oauthTokens");
6
+ // StreamBy's metadata as an OAuth protected resource (RFC 9728), open to anyone: an MCP client
7
+ // that got a 401 reads it to know which authorization server to sign the user in with. The
8
+ // host serves the same document at its root too, where RFC 9728 puts it.
9
+ function oauthResourceRouter(config) {
10
+ const router = (0, express_1.Router)();
11
+ router.get('/.well-known/oauth-protected-resource', (_req, res) => {
12
+ if (!config.oauth)
13
+ return res.status(404).json({ error: 'Not an OAuth protected resource' });
14
+ res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'public, max-age=300' });
15
+ return res.json((0, oauthTokens_1.protectedResourceMetadata)(config.oauth));
16
+ });
17
+ return router;
18
+ }
@@ -12,6 +12,7 @@ const auth_2 = require("../../services/auth");
12
12
  const apiTokens_1 = require("../../services/apiTokens");
13
13
  const nosqlDb_1 = require("../../utils/nosqlDb");
14
14
  const production_1 = require("../../services/production");
15
+ const jobStore_1 = require("../../services/jobStore");
15
16
  function projectRouter(config) {
16
17
  const router = (0, express_1.Router)();
17
18
  const Project = (0, manager_1.getModel)('projects');
@@ -254,9 +255,12 @@ function projectRouter(config) {
254
255
  return res.status(403).json({ message: 'Only a project admin can delete it' });
255
256
  }
256
257
  await Project.useDbType(project.dbType).delete({ _id: projectId });
257
- // Its tokens have nothing left to reach, and its production data nothing to belong to.
258
+ // Its tokens have nothing left to reach, and its production data and jobs nothing to
259
+ // belong to.
258
260
  await (0, apiTokens_1.deleteProjectApiTokens)(String(projectId));
259
261
  await (0, production_1.deleteProductionData)(String(projectId));
262
+ if (projectId)
263
+ await (0, jobStore_1.jobStore)().deleteProject(String(projectId));
260
264
  const allProjects = await Project.find({});
261
265
  const projects = allProjects
262
266
  .filter(project => {
@@ -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
+ }
@@ -45,11 +45,11 @@ class Model {
45
45
  // Handle members filter for SQL projects
46
46
  const userId = processedFilter.members.$elemMatch.userId;
47
47
  const schema = this.schema || 'streamby'; // Default to 'streamby' if schema not provided
48
- const query = `
49
- SELECT p.*, pm."userId" as memberUserId, pm.role as memberRole, pm.status as memberStatus, pm.archived as memberArchived
50
- FROM "${schema}"."projects" p
51
- JOIN "${schema}"."project_members" pm ON p.id = pm."projectId"
52
- WHERE pm."userId" = $1
48
+ const query = `
49
+ SELECT p.*, pm."userId" as memberUserId, pm.role as memberRole, pm.status as memberStatus, pm.archived as memberArchived
50
+ FROM "${schema}"."projects" p
51
+ JOIN "${schema}"."project_members" pm ON p.id = pm."projectId"
52
+ WHERE pm."userId" = $1
53
53
  `;
54
54
  const result = await connection.query(query, [userId]);
55
55
  sqlResults = result.rows.map(row => ({
@@ -219,10 +219,10 @@ class Model {
219
219
  else {
220
220
  const matchKey = value.id !== undefined ? 'id' : Object.keys(value)[0];
221
221
  const matchVal = String(value[matchKey]);
222
- await connection.query(`UPDATE ${tbl} SET "${field}" = (
223
- SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
224
- FROM jsonb_array_elements("${field}") elem
225
- WHERE elem->>'${matchKey}' != $1
222
+ await connection.query(`UPDATE ${tbl} SET "${field}" = (
223
+ SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
224
+ FROM jsonb_array_elements("${field}") elem
225
+ WHERE elem->>'${matchKey}' != $1
226
226
  ) WHERE id = $2`, [matchVal, id]);
227
227
  }
228
228
  }