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.
- package/CHANGELOG.md +84 -0
- package/LICENSE +661 -661
- package/README.md +785 -573
- package/dist/adapters/database/connectionManager.js +182 -124
- package/dist/adapters/database/sql.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +9 -1
- package/dist/mcp/audit.d.ts +5 -2
- package/dist/mcp/audit.js +8 -0
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools.js +41 -0
- package/dist/middleware/createRouter.js +14 -1
- package/dist/middleware/routes/ai.js +3 -3
- package/dist/middleware/routes/deliverable.d.ts +4 -2
- package/dist/middleware/routes/deliverable.js +16 -7
- package/dist/middleware/routes/mcp.js +11 -4
- package/dist/middleware/routes/media.js +7 -6
- package/dist/middleware/routes/{apiConnection.d.ts → oauthResource.d.ts} +1 -1
- package/dist/middleware/routes/oauthResource.js +18 -0
- package/dist/middleware/routes/project.js +5 -1
- package/dist/middleware/routes/renderFarm.js +4 -77
- package/dist/middleware/routes/storage.js +13 -27
- package/dist/middleware/routes/worker.d.ts +7 -0
- package/dist/middleware/routes/worker.js +125 -0
- package/dist/middleware/routes/workerCallback.d.ts +5 -0
- package/dist/middleware/routes/workerCallback.js +31 -0
- package/dist/models/Model.js +9 -9
- package/dist/services/aiProcessor.js +11 -11
- package/dist/services/apiTokenPermissions.js +6 -5
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +36 -10
- package/dist/services/executor.js +10 -10
- package/dist/services/jobQueue.d.ts +6 -4
- package/dist/services/jobQueue.js +28 -16
- package/dist/services/jobStore.d.ts +27 -0
- package/dist/services/jobStore.js +177 -0
- package/dist/services/mediaProcessor.js +16 -16
- package/dist/services/oauthTokens.d.ts +60 -0
- package/dist/services/oauthTokens.js +164 -0
- package/dist/services/vfxProcessor.js +12 -12
- package/dist/services/workerJobs.d.ts +73 -0
- package/dist/services/workerJobs.js +459 -0
- package/dist/services/workers.d.ts +78 -0
- package/dist/services/workers.js +179 -0
- package/dist/types/index.d.ts +64 -2
- package/dist/utils/assetCategories.d.ts +4 -0
- package/dist/utils/assetCategories.js +21 -0
- package/dist/utils/builtinAccess.d.ts +1 -1
- package/dist/utils/builtinAccess.js +4 -3
- package/dist/utils/privateAddress.d.ts +10 -0
- package/dist/utils/privateAddress.js +85 -0
- package/package.json +1 -1
- package/dist/middleware/routes/apiConnection.js +0 -100
- package/dist/middleware/routes/credential.d.ts +0 -3
- package/dist/middleware/routes/credential.js +0 -80
- package/dist/services/apiConnection.d.ts +0 -18
- package/dist/services/apiConnection.js +0 -65
- package/dist/services/credential.d.ts +0 -8
- package/dist/services/credential.js +0 -70
- package/dist/services/pipeline.d.ts +0 -2
- package/dist/services/pipeline.js +0 -292
- package/dist/utils/crypto.d.ts +0 -3
- 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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
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,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
|
+
}
|
package/dist/models/Model.js
CHANGED
|
@@ -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
|
}
|