streamby-core 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +141 -44
- package/dist/adapters/database/connectionManager.js +24 -11
- package/dist/adapters/storage/s3.d.ts +3 -2
- package/dist/adapters/storage/s3.js +18 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/mcp/server.d.ts +4 -3
- package/dist/mcp/server.js +25 -10
- package/dist/mcp/tools.d.ts +7 -2
- package/dist/mcp/tools.js +187 -35
- package/dist/mcp/workflowNodes.d.ts +24 -0
- package/dist/mcp/workflowNodes.js +48 -0
- package/dist/middleware/createRouter.js +2 -0
- package/dist/middleware/routes/apiToken.js +77 -14
- package/dist/middleware/routes/deliverable.d.ts +3 -0
- package/dist/middleware/routes/deliverable.js +131 -0
- package/dist/middleware/routes/export.js +2 -0
- package/dist/middleware/routes/member.js +8 -0
- package/dist/middleware/routes/production.js +60 -67
- package/dist/middleware/routes/project.js +15 -3
- package/dist/middleware/routes/storage.js +115 -24
- package/dist/middleware/routes/subscription.js +3 -0
- package/dist/services/apiTokenPermissions.d.ts +16 -0
- package/dist/services/apiTokenPermissions.js +102 -0
- package/dist/services/apiTokens.d.ts +38 -14
- package/dist/services/apiTokens.js +134 -40
- package/dist/services/assetVersions.d.ts +48 -0
- package/dist/services/assetVersions.js +84 -0
- package/dist/services/auth.d.ts +4 -1
- package/dist/services/auth.js +24 -18
- package/dist/services/deliverables.d.ts +26 -0
- package/dist/services/deliverables.js +55 -0
- package/dist/services/mediaProcessor.js +3 -1
- package/dist/services/production.d.ts +2 -0
- package/dist/services/production.js +13 -0
- package/dist/services/productionFields.d.ts +16 -0
- package/dist/services/productionFields.js +80 -0
- package/dist/services/signedUrls.d.ts +6 -0
- package/dist/services/signedUrls.js +9 -0
- package/dist/types/index.d.ts +12 -4
- package/dist/utils/auth.d.ts +12 -1
- package/dist/utils/auth.js +23 -2
- package/dist/utils/nosqlDb.d.ts +2 -0
- package/dist/utils/nosqlDb.js +11 -0
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -8,19 +8,20 @@ const projectId = id('project');
|
|
|
8
8
|
const enc = encodeURIComponent;
|
|
9
9
|
const byProject = ({ projectId }) => projectId;
|
|
10
10
|
const CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
11
|
+
const STATUSES = ['todo', 'in_progress', 'review', 'done'];
|
|
11
12
|
const shotFields = {
|
|
12
13
|
description: zod_1.z.string().optional(),
|
|
13
|
-
status: zod_1.z.
|
|
14
|
-
assignedTo: zod_1.z.string().optional().describe("The
|
|
14
|
+
status: zod_1.z.enum(STATUSES).optional(),
|
|
15
|
+
assignedTo: zod_1.z.array(zod_1.z.string()).optional().describe("The assignees' user ids: members of the project"),
|
|
15
16
|
assetId: zod_1.z.string().optional(),
|
|
16
17
|
exportId: zod_1.z.string().optional(),
|
|
17
18
|
dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
|
|
18
19
|
order: zod_1.z.number().optional(),
|
|
19
20
|
};
|
|
20
21
|
const taskFields = {
|
|
21
|
-
status: zod_1.z.
|
|
22
|
-
priority: zod_1.z.
|
|
23
|
-
assignedTo: zod_1.z.string().optional().describe("The assignee's user id"),
|
|
22
|
+
status: zod_1.z.enum(STATUSES).optional(),
|
|
23
|
+
priority: zod_1.z.enum(['low', 'medium', 'high']).optional(),
|
|
24
|
+
assignedTo: zod_1.z.string().optional().describe("The assignee's user id: a member of the project"),
|
|
24
25
|
dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
|
|
25
26
|
notes: zod_1.z.string().optional(),
|
|
26
27
|
};
|
|
@@ -38,21 +39,21 @@ const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([,
|
|
|
38
39
|
exports.TOOLS = [
|
|
39
40
|
// ── Reading ──
|
|
40
41
|
tool({
|
|
41
|
-
name: 'list_projects', title: 'List projects',
|
|
42
|
+
name: 'list_projects', title: 'List projects', permissions: [],
|
|
42
43
|
description: 'The StreamBy projects this token reaches.',
|
|
43
44
|
input: {},
|
|
44
45
|
call: () => ({ method: 'GET', path: '/projects' }),
|
|
45
46
|
pick: (data) => data.projects,
|
|
46
47
|
}),
|
|
47
48
|
tool({
|
|
48
|
-
name: 'get_project', title: 'Get a project',
|
|
49
|
+
name: 'get_project', title: 'Get a project', permissions: ['project:read'],
|
|
49
50
|
description: "A project's details: its name, description, members, exports and connections.",
|
|
50
51
|
input: { projectId },
|
|
51
52
|
projectOf: byProject,
|
|
52
53
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}` }),
|
|
53
54
|
}),
|
|
54
55
|
tool({
|
|
55
|
-
name: 'list_assets', title: 'List assets',
|
|
56
|
+
name: 'list_assets', title: 'List assets', permissions: ['assets:read'],
|
|
56
57
|
description: "A project's assets of a category (images, audios, videos, 3d-models), optionally in one storage connection, and optionally only those whose name contains `query`.",
|
|
57
58
|
input: {
|
|
58
59
|
projectId,
|
|
@@ -77,42 +78,42 @@ exports.TOOLS = [
|
|
|
77
78
|
},
|
|
78
79
|
}),
|
|
79
80
|
tool({
|
|
80
|
-
name: 'get_asset_metadata', title: "Get an asset's metadata",
|
|
81
|
+
name: 'get_asset_metadata', title: "Get an asset's metadata", permissions: ['assets:read'],
|
|
81
82
|
description: "An asset's metadata (technical data and custom tags).",
|
|
82
83
|
input: { projectId, fileId: id('asset') },
|
|
83
84
|
projectOf: byProject,
|
|
84
85
|
call: ({ projectId, fileId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata` }),
|
|
85
86
|
}),
|
|
86
87
|
tool({
|
|
87
|
-
name: 'get_asset_rights', title: "Get an asset's rights",
|
|
88
|
+
name: 'get_asset_rights', title: "Get an asset's rights", permissions: ['rights:read'],
|
|
88
89
|
description: "An asset's rights: holder, license, territory, expiry and restrictions.",
|
|
89
90
|
input: { projectId, assetId: id('asset') },
|
|
90
91
|
projectOf: byProject,
|
|
91
92
|
call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights` }),
|
|
92
93
|
}),
|
|
93
94
|
tool({
|
|
94
|
-
name: 'get_workflow', title: "Get a project's workflow",
|
|
95
|
+
name: 'get_workflow', title: "Get a project's workflow", permissions: ['workflow:read'],
|
|
95
96
|
description: "A project's workflow (its node graph).",
|
|
96
97
|
input: { projectId },
|
|
97
98
|
projectOf: byProject,
|
|
98
99
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/workflow` }),
|
|
99
100
|
}),
|
|
100
101
|
tool({
|
|
101
|
-
name: 'list_pipelines', title: 'List pipelines',
|
|
102
|
+
name: 'list_pipelines', title: 'List pipelines', permissions: ['workflow:read'],
|
|
102
103
|
description: "A project's pipelines.",
|
|
103
104
|
input: { projectId },
|
|
104
105
|
projectOf: byProject,
|
|
105
106
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines` }),
|
|
106
107
|
}),
|
|
107
108
|
tool({
|
|
108
|
-
name: 'get_pipeline', title: 'Get a pipeline',
|
|
109
|
+
name: 'get_pipeline', title: 'Get a pipeline', permissions: ['workflow:read'],
|
|
109
110
|
description: 'A pipeline, with its node schema.',
|
|
110
111
|
input: { projectId, pipelineId: id('pipeline') },
|
|
111
112
|
projectOf: byProject,
|
|
112
113
|
call: ({ projectId, pipelineId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}` }),
|
|
113
114
|
}),
|
|
114
115
|
tool({
|
|
115
|
-
name: 'list_exports', title: 'List exports',
|
|
116
|
+
name: 'list_exports', title: 'List exports', permissions: ['project:read', 'exports:read'],
|
|
116
117
|
description: "A project's exports (its published endpoints).",
|
|
117
118
|
input: { projectId },
|
|
118
119
|
projectOf: byProject,
|
|
@@ -120,34 +121,34 @@ exports.TOOLS = [
|
|
|
120
121
|
pick: (data) => data?.project?.exports ?? data?.exports ?? [],
|
|
121
122
|
}),
|
|
122
123
|
tool({
|
|
123
|
-
name: 'get_export', title: 'Get an export',
|
|
124
|
+
name: 'get_export', title: 'Get an export', permissions: ['exports:read'],
|
|
124
125
|
description: 'An export, with its node schema.',
|
|
125
126
|
input: { projectId, exportId: id('export') },
|
|
126
127
|
projectOf: byProject,
|
|
127
128
|
call: ({ projectId, exportId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/exports/${enc(exportId)}` }),
|
|
128
129
|
}),
|
|
129
130
|
tool({
|
|
130
|
-
name: 'get_job', title: 'Get a job',
|
|
131
|
+
name: 'get_job', title: 'Get a job', permissions: ['jobs:read'],
|
|
131
132
|
description: "A job's status and progress (transcoding, rendering, ingesting...).",
|
|
132
133
|
input: { jobId: id('job') },
|
|
133
134
|
call: ({ jobId }) => ({ method: 'GET', path: `/jobs/${enc(jobId)}` }),
|
|
134
135
|
}),
|
|
135
136
|
tool({
|
|
136
|
-
name: 'list_sequences', title: 'List production sequences',
|
|
137
|
+
name: 'list_sequences', title: 'List production sequences', permissions: ['production:read'],
|
|
137
138
|
description: "A project's production sequences.",
|
|
138
139
|
input: { projectId },
|
|
139
140
|
projectOf: byProject,
|
|
140
141
|
call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences` }),
|
|
141
142
|
}),
|
|
142
143
|
tool({
|
|
143
|
-
name: 'list_shots', title: 'List shots',
|
|
144
|
+
name: 'list_shots', title: 'List shots', permissions: ['production:read'],
|
|
144
145
|
description: "A production sequence's shots.",
|
|
145
146
|
input: { projectId, sequenceId: id('sequence') },
|
|
146
147
|
projectOf: byProject,
|
|
147
148
|
call: ({ projectId, sequenceId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots` }),
|
|
148
149
|
}),
|
|
149
150
|
tool({
|
|
150
|
-
name: 'list_tasks', title: 'List shot tasks',
|
|
151
|
+
name: 'list_tasks', title: 'List shot tasks', permissions: ['production:read'],
|
|
151
152
|
description: "A shot's tasks.",
|
|
152
153
|
input: { projectId, shotId: id('shot') },
|
|
153
154
|
projectOf: byProject,
|
|
@@ -155,73 +156,224 @@ exports.TOOLS = [
|
|
|
155
156
|
}),
|
|
156
157
|
// ── Writing ──
|
|
157
158
|
tool({
|
|
158
|
-
name: 'update_asset_metadata', title: "Update an asset's custom tags", write: true,
|
|
159
|
-
description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had.",
|
|
159
|
+
name: 'update_asset_metadata', title: "Update an asset's custom tags", permissions: ['assets:write'], write: true,
|
|
160
|
+
description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had. They show in the file's Metadata tab, in Storage.",
|
|
160
161
|
input: { projectId, fileId: id('asset'), customTags: zod_1.z.record(zod_1.z.string(), zod_1.z.string()) },
|
|
161
162
|
projectOf: byProject,
|
|
162
163
|
call: ({ projectId, fileId, customTags }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata`, body: { customTags } }),
|
|
163
164
|
}),
|
|
164
165
|
tool({
|
|
165
|
-
name: 'set_asset_rights', title: "Set an asset's rights", write: true,
|
|
166
|
-
description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes.",
|
|
166
|
+
name: 'set_asset_rights', title: "Set an asset's rights", permissions: ['rights:write'], write: true,
|
|
167
|
+
description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes. They show in the file's Rights tab, in Storage.",
|
|
167
168
|
input: { projectId, assetId: id('asset'), ...rightsFields },
|
|
168
169
|
projectOf: byProject,
|
|
169
170
|
call: ({ projectId, assetId, ...rights }) => ({ method: 'PUT', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights`, body: defined(rights) }),
|
|
170
171
|
}),
|
|
171
172
|
tool({
|
|
172
|
-
name: 'create_pipeline', title: 'Create a pipeline', write: true,
|
|
173
|
-
description:
|
|
173
|
+
name: 'create_pipeline', title: 'Create a pipeline', permissions: ['workflow:write'], write: true,
|
|
174
|
+
description: "Creates a pipeline in a project. It shows under Pipelines, and the workflow canvas draws its node.",
|
|
174
175
|
input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
175
176
|
projectOf: byProject,
|
|
176
177
|
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/pipelines`, body: defined(fields) }),
|
|
177
178
|
}),
|
|
178
179
|
tool({
|
|
179
|
-
name: 'update_pipeline', title: 'Update a pipeline', write: true,
|
|
180
|
+
name: 'update_pipeline', title: 'Update a pipeline', permissions: ['workflow:write'], write: true,
|
|
180
181
|
description: "Changes a pipeline's name, description or order.",
|
|
181
182
|
input: { projectId, pipelineId: id('pipeline'), name: zod_1.z.string().min(1).optional(), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
182
183
|
projectOf: byProject,
|
|
183
184
|
call: ({ projectId, pipelineId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}`, body: defined(fields) }),
|
|
184
185
|
}),
|
|
185
186
|
tool({
|
|
186
|
-
name: 'create_sequence', title: 'Create a production sequence', write: true,
|
|
187
|
-
description:
|
|
187
|
+
name: 'create_sequence', title: 'Create a production sequence', permissions: ['production:write', 'workflow:write'], write: true, workflowNode: 'production',
|
|
188
|
+
description: "Creates a production sequence in a project, shown in its Production board. If the project's workflow has no production node yet, it first adds one (a Shot node, as dropping it on the canvas does), so the project's Production section shows it: that's why it also needs workflow:write.",
|
|
188
189
|
input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
189
190
|
projectOf: byProject,
|
|
190
191
|
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences`, body: defined(fields) }),
|
|
191
192
|
}),
|
|
192
193
|
tool({
|
|
193
|
-
name: 'update_sequence', title: 'Update a production sequence', write: true,
|
|
194
|
+
name: 'update_sequence', title: 'Update a production sequence', permissions: ['production:write'], write: true,
|
|
194
195
|
description: "Changes a sequence's name, description or order.",
|
|
195
196
|
input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1).optional(), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
196
197
|
projectOf: byProject,
|
|
197
198
|
call: ({ projectId, sequenceId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}`, body: defined(fields) }),
|
|
198
199
|
}),
|
|
199
200
|
tool({
|
|
200
|
-
name: 'create_shot', title: 'Create a shot', write: true,
|
|
201
|
-
description:
|
|
201
|
+
name: 'create_shot', title: 'Create a shot', permissions: ['production:write', 'workflow:write'], write: true, workflowNode: 'production',
|
|
202
|
+
description: "Creates a shot in a production sequence, shown in its Production board. If the project's workflow has no production node yet, it first adds one (a Shot node, as dropping it on the canvas does), so the project's Production section shows it: that's why it also needs workflow:write.",
|
|
202
203
|
input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1), ...shotFields },
|
|
203
204
|
projectOf: byProject,
|
|
204
205
|
call: ({ projectId, sequenceId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots`, body: defined(fields) }),
|
|
205
206
|
}),
|
|
206
207
|
tool({
|
|
207
|
-
name: 'update_shot', title: 'Update a shot', write: true,
|
|
208
|
+
name: 'update_shot', title: 'Update a shot', permissions: ['production:write'], write: true,
|
|
208
209
|
description: 'Changes a shot (its name, status, assignee, due date...).',
|
|
209
210
|
input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1).optional(), ...shotFields },
|
|
210
211
|
projectOf: byProject,
|
|
211
212
|
call: ({ projectId, shotId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}`, body: defined(fields) }),
|
|
212
213
|
}),
|
|
213
214
|
tool({
|
|
214
|
-
name: 'create_task', title: 'Create a shot task', write: true,
|
|
215
|
-
description:
|
|
215
|
+
name: 'create_task', title: 'Create a shot task', permissions: ['production:write', 'workflow:write'], write: true, workflowNode: 'production',
|
|
216
|
+
description: "Creates a task in a shot, shown in the shot's panel in the Production board. If the project's workflow has no production node yet, it first adds one (a Shot node, as dropping it on the canvas does), so the project's Production section shows it: that's why it also needs workflow:write.",
|
|
216
217
|
input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1), ...taskFields },
|
|
217
218
|
projectOf: byProject,
|
|
218
219
|
call: ({ projectId, shotId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks`, body: defined(fields) }),
|
|
219
220
|
}),
|
|
220
221
|
tool({
|
|
221
|
-
name: 'update_task', title: 'Update a shot task', write: true,
|
|
222
|
+
name: 'update_task', title: 'Update a shot task', permissions: ['production:write'], write: true,
|
|
222
223
|
description: 'Changes a task (its name, status, priority, assignee, due date, notes).',
|
|
223
224
|
input: { projectId, shotId: id('shot'), taskId: id('task'), name: zod_1.z.string().min(1).optional(), ...taskFields },
|
|
224
225
|
projectOf: byProject,
|
|
225
226
|
call: ({ projectId, shotId, taskId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}`, body: defined(fields) }),
|
|
226
227
|
}),
|
|
228
|
+
// ── Files: uploading, downloading, deliverables ──
|
|
229
|
+
// The file itself never goes through the conversation: these tools give a signed URL, and
|
|
230
|
+
// the user's own tool (Blender's or Godot's MCP, a script) uploads or downloads it directly.
|
|
231
|
+
tool({
|
|
232
|
+
name: 'list_asset_versions', title: "List an asset's versions", permissions: ['assets:read'],
|
|
233
|
+
description: "An asset's versions, newest first: each one's id, number, file name and when it was made. Any of them can be downloaded (get_asset_download_url).",
|
|
234
|
+
input: { projectId, assetId: id('asset') },
|
|
235
|
+
projectOf: byProject,
|
|
236
|
+
call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/versions` }),
|
|
237
|
+
pick: (data) => (data?.versions ?? []).map((v) => ({
|
|
238
|
+
versionId: v.versionId, version: v.version, fileName: v.fileName ?? null, contentType: v.contentType ?? null, createdAt: v.createdAt,
|
|
239
|
+
})),
|
|
240
|
+
}),
|
|
241
|
+
tool({
|
|
242
|
+
name: 'request_asset_upload', title: 'Request an upload URL', permissions: ['assets:write'], write: true,
|
|
243
|
+
description: [
|
|
244
|
+
"Makes a new asset (or, with assetId, a new version of one) and gives a signed URL to upload its file to, as the UI does.",
|
|
245
|
+
"It doesn't upload anything: the file is sent by the tool that has it (for example Blender's MCP, running Python), with an HTTP PUT of its bytes to `upload.url` and the `upload.headers` given, before `expiresAt`.",
|
|
246
|
+
"The URL works for that file only. A new version keeps the asset's URL (it replaces what it serves); the earlier ones are archived and can still be downloaded.",
|
|
247
|
+
"Categories: images, audios, videos, 3d-models (.glb, .gltf, .obj, .fbx, .stl, .ply); contentType must match it.",
|
|
248
|
+
].join(' '),
|
|
249
|
+
input: {
|
|
250
|
+
projectId,
|
|
251
|
+
category: zod_1.z.enum(CATEGORIES),
|
|
252
|
+
fileName: zod_1.z.string().min(1).describe('The file name, with its extension (e.g. shot010_layout.glb)'),
|
|
253
|
+
contentType: zod_1.z.string().min(1).describe('Its MIME type (e.g. model/gltf-binary, image/png, video/mp4)'),
|
|
254
|
+
assetId: zod_1.z.string().optional().describe('An existing asset to upload a new version of; a new asset if not given'),
|
|
255
|
+
connectionId: zod_1.z.string().optional().describe("A storage connection to store it in; the project's built-in storage if not given"),
|
|
256
|
+
},
|
|
257
|
+
projectOf: byProject,
|
|
258
|
+
call: ({ projectId, category, fileName, contentType, assetId, connectionId }) => {
|
|
259
|
+
const query = new URLSearchParams({ fileName, contentType, ...(connectionId && !assetId ? { category } : {}) });
|
|
260
|
+
const files = connectionId ? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}` : `/projects/${enc(projectId)}/storage`;
|
|
261
|
+
const path = assetId
|
|
262
|
+
? `${files}/files/${enc(assetId)}/replace-url`
|
|
263
|
+
: connectionId ? `${files}/upload-url` : `${files}/${category}/upload-url`;
|
|
264
|
+
return { method: 'GET', path: `${path}?${query}` };
|
|
265
|
+
},
|
|
266
|
+
pick: (data, { contentType }) => ({
|
|
267
|
+
assetId: data?.fileId,
|
|
268
|
+
...(data?.versionId ? { versionId: data.versionId, version: data.version } : { version: 1 }),
|
|
269
|
+
upload: { method: 'PUT', url: data?.url, headers: { 'Content-Type': contentType } },
|
|
270
|
+
expiresAt: data?.expiresAt ?? null,
|
|
271
|
+
}),
|
|
272
|
+
}),
|
|
273
|
+
tool({
|
|
274
|
+
name: 'get_asset_download_url', title: 'Get a download URL', permissions: ['assets:read'],
|
|
275
|
+
description: "A short-lived signed URL to download an asset's file (its latest version, or the one given), for the tool that will use it (for example Blender's or Godot's MCP) to fetch with an HTTP GET before `expiresAt`. It works for that file only.",
|
|
276
|
+
input: { projectId, assetId: id('asset'), versionId: zod_1.z.string().optional().describe('A version (list_asset_versions); the latest if not given') },
|
|
277
|
+
projectOf: byProject,
|
|
278
|
+
call: ({ projectId, assetId, versionId }) => ({
|
|
279
|
+
method: 'GET',
|
|
280
|
+
path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/download-url${versionId ? `?${new URLSearchParams({ versionId })}` : ''}`,
|
|
281
|
+
}),
|
|
282
|
+
}),
|
|
283
|
+
tool({
|
|
284
|
+
name: 'list_deliverables', title: 'List deliverables', permissions: ['assets:read'],
|
|
285
|
+
description: "A project's deliverables (assets delivered for a shot or an export), of one shot or export if given: each one's asset, version, note and what it's for.",
|
|
286
|
+
input: {
|
|
287
|
+
projectId,
|
|
288
|
+
shotId: zod_1.z.string().optional().describe("A shot's id: only its deliverables"),
|
|
289
|
+
exportId: zod_1.z.string().optional().describe("An export's id: only its deliverables"),
|
|
290
|
+
},
|
|
291
|
+
projectOf: byProject,
|
|
292
|
+
call: ({ projectId, shotId, exportId }) => {
|
|
293
|
+
const query = new URLSearchParams(defined({ shotId, exportId })).toString();
|
|
294
|
+
return { method: 'GET', path: `/projects/${enc(projectId)}/deliverables${query ? `?${query}` : ''}` };
|
|
295
|
+
},
|
|
296
|
+
pick: (data) => data?.deliverables ?? [],
|
|
297
|
+
}),
|
|
298
|
+
tool({
|
|
299
|
+
name: 'mark_deliverable', title: 'Mark a deliverable', permissions: ['assets:write'], write: true,
|
|
300
|
+
description: "Marks an asset (or one of its versions) as a deliverable of a production shot or of an export. It shows in the shot's panel in the Production board, or in the export's Deliverables tab. Give shotId or exportId, one of them. Upload the file first (request_asset_upload).",
|
|
301
|
+
input: {
|
|
302
|
+
projectId,
|
|
303
|
+
assetId: id('asset'),
|
|
304
|
+
versionId: zod_1.z.string().optional().describe('A version of it; without one, the asset as it is (its latest version)'),
|
|
305
|
+
shotId: zod_1.z.string().optional().describe("The shot it's delivered for"),
|
|
306
|
+
exportId: zod_1.z.string().optional().describe("The export it's delivered for"),
|
|
307
|
+
note: zod_1.z.string().max(1000).optional(),
|
|
308
|
+
},
|
|
309
|
+
projectOf: byProject,
|
|
310
|
+
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/deliverables`, body: defined(fields) }),
|
|
311
|
+
pick: (data) => data?.deliverable,
|
|
312
|
+
}),
|
|
313
|
+
tool({
|
|
314
|
+
name: 'remove_deliverable', title: 'Remove a deliverable', permissions: ['assets:write'], write: true,
|
|
315
|
+
description: 'Unmarks a deliverable. Its asset stays where it is.',
|
|
316
|
+
input: { projectId, deliverableId: id('deliverable') },
|
|
317
|
+
projectOf: byProject,
|
|
318
|
+
call: ({ projectId, deliverableId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/deliverables/${enc(deliverableId)}` }),
|
|
319
|
+
}),
|
|
320
|
+
// ── Projects ──
|
|
321
|
+
tool({
|
|
322
|
+
name: 'create_project', title: 'Create a project', permissions: ['projects:create'], write: true,
|
|
323
|
+
description: 'Creates a StreamBy project, its maker being its admin, as from "New project" in the UI. The token reaches it from then on.',
|
|
324
|
+
input: {
|
|
325
|
+
name: zod_1.z.string().min(1),
|
|
326
|
+
description: zod_1.z.string().optional(),
|
|
327
|
+
public: zod_1.z.boolean().optional().describe('Whether it shows in Explore (true if not given; a private one needs a subscriber plan)'),
|
|
328
|
+
category: zod_1.z.string().optional(),
|
|
329
|
+
},
|
|
330
|
+
call: (fields) => ({ method: 'POST', path: '/projects/create', body: defined(fields) }),
|
|
331
|
+
pick: (data) => ({ projectId: data?.projectId, message: data?.message }),
|
|
332
|
+
}),
|
|
333
|
+
tool({
|
|
334
|
+
name: 'delete_project', title: 'Delete a project', permissions: ['project:delete'], write: true, destructive: true,
|
|
335
|
+
description: 'Deletes a project, with everything in it, for every member. It cannot be undone.',
|
|
336
|
+
input: { projectId },
|
|
337
|
+
projectOf: byProject,
|
|
338
|
+
call: ({ projectId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}` }),
|
|
339
|
+
pick: (data) => ({ message: data?.message }),
|
|
340
|
+
}),
|
|
341
|
+
// ── Deleting ──
|
|
342
|
+
tool({
|
|
343
|
+
name: 'delete_asset', title: 'Delete an asset', permissions: ['assets:delete'], write: true, destructive: true,
|
|
344
|
+
description: "Deletes an asset's file, from the project's built-in storage or from one of its storage connections. It cannot be undone.",
|
|
345
|
+
input: {
|
|
346
|
+
projectId,
|
|
347
|
+
fileId: id('asset'),
|
|
348
|
+
connectionId: zod_1.z.string().optional().describe("The storage connection it's in; the project's built-in storage if not given"),
|
|
349
|
+
},
|
|
350
|
+
projectOf: byProject,
|
|
351
|
+
call: ({ projectId, fileId, connectionId }) => ({
|
|
352
|
+
method: 'DELETE',
|
|
353
|
+
path: connectionId
|
|
354
|
+
? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}/files/${enc(fileId)}`
|
|
355
|
+
: `/projects/${enc(projectId)}/storage/files/${enc(fileId)}`,
|
|
356
|
+
}),
|
|
357
|
+
}),
|
|
358
|
+
tool({
|
|
359
|
+
name: 'delete_sequence', title: 'Delete a production sequence', permissions: ['production:delete'], write: true, destructive: true,
|
|
360
|
+
description: 'Deletes a production sequence, with its shots. It cannot be undone.',
|
|
361
|
+
input: { projectId, sequenceId: id('sequence') },
|
|
362
|
+
projectOf: byProject,
|
|
363
|
+
call: ({ projectId, sequenceId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}` }),
|
|
364
|
+
}),
|
|
365
|
+
tool({
|
|
366
|
+
name: 'delete_shot', title: 'Delete a shot', permissions: ['production:delete'], write: true, destructive: true,
|
|
367
|
+
description: 'Deletes a shot, with its tasks. It cannot be undone.',
|
|
368
|
+
input: { projectId, shotId: id('shot') },
|
|
369
|
+
projectOf: byProject,
|
|
370
|
+
call: ({ projectId, shotId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}` }),
|
|
371
|
+
}),
|
|
372
|
+
tool({
|
|
373
|
+
name: 'delete_task', title: 'Delete a shot task', permissions: ['production:delete'], write: true, destructive: true,
|
|
374
|
+
description: "Deletes a shot's task. It cannot be undone.",
|
|
375
|
+
input: { projectId, shotId: id('shot'), taskId: id('task') },
|
|
376
|
+
projectOf: byProject,
|
|
377
|
+
call: ({ projectId, shotId, taskId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}` }),
|
|
378
|
+
}),
|
|
227
379
|
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Auth } from '../types';
|
|
2
|
+
import type { ApiCaller } from './server';
|
|
3
|
+
export type WorkflowNodeRequirement = 'production';
|
|
4
|
+
/** A Shot node, as the UI makes one dropped on the canvas without a position. */
|
|
5
|
+
export declare const newShotNode: () => {
|
|
6
|
+
id: string;
|
|
7
|
+
type: string;
|
|
8
|
+
position: {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
};
|
|
12
|
+
data: {
|
|
13
|
+
label: string;
|
|
14
|
+
subtitle: string;
|
|
15
|
+
bgColor: string;
|
|
16
|
+
iconColor: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Makes sure a project's workflow has what `requirement` needs, adding it (through the REST
|
|
21
|
+
* API, as the caller) if it's missing. An error to answer with, or null to go on. A project
|
|
22
|
+
* the caller doesn't reach is left alone: the call that follows answers for it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ensureWorkflowNode(requirement: WorkflowNodeRequirement, projectId: string | undefined, auth: Auth, api: ApiCaller): Promise<string | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.newShotNode = void 0;
|
|
7
|
+
exports.ensureWorkflowNode = ensureWorkflowNode;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const manager_1 = require("../models/manager");
|
|
10
|
+
const auth_1 = require("../utils/auth");
|
|
11
|
+
const auth_2 = require("../services/auth");
|
|
12
|
+
// The node types that make a workflow's production section (as the UI groups them).
|
|
13
|
+
const PRODUCTION_NODE_TYPES = new Set(['shotNode', 'assemblyNode', 'masterNode']);
|
|
14
|
+
/** A Shot node, as the UI makes one dropped on the canvas without a position. */
|
|
15
|
+
const newShotNode = () => ({
|
|
16
|
+
id: `shotNode-${crypto_1.default.randomUUID()}`,
|
|
17
|
+
type: 'shotNode',
|
|
18
|
+
position: { x: 240 + Math.random() * 60, y: 300 + Math.random() * 40 },
|
|
19
|
+
data: { label: 'Shot', subtitle: 'Raw video clip', bgColor: '#0e2537', iconColor: '#38B6FF' },
|
|
20
|
+
});
|
|
21
|
+
exports.newShotNode = newShotNode;
|
|
22
|
+
/**
|
|
23
|
+
* Makes sure a project's workflow has what `requirement` needs, adding it (through the REST
|
|
24
|
+
* API, as the caller) if it's missing. An error to answer with, or null to go on. A project
|
|
25
|
+
* the caller doesn't reach is left alone: the call that follows answers for it.
|
|
26
|
+
*/
|
|
27
|
+
async function ensureWorkflowNode(requirement, projectId, auth, api) {
|
|
28
|
+
if (requirement !== 'production' || !projectId)
|
|
29
|
+
return null;
|
|
30
|
+
if (!(0, auth_2.scopeReachesProject)(auth.apiToken, projectId))
|
|
31
|
+
return null;
|
|
32
|
+
const project = await (0, manager_1.getModel)('projects').findOne({ _id: projectId });
|
|
33
|
+
if (!project || !(0, auth_1.activeMemberOf)(project, auth.userId))
|
|
34
|
+
return null;
|
|
35
|
+
const schema = project.workflow?.nodeSchema ?? null;
|
|
36
|
+
const nodes = Array.isArray(schema?.nodes) ? schema.nodes : [];
|
|
37
|
+
if (nodes.some(node => PRODUCTION_NODE_TYPES.has(node?.type)))
|
|
38
|
+
return null;
|
|
39
|
+
const answer = await api({
|
|
40
|
+
method: 'PATCH',
|
|
41
|
+
path: `/projects/${encodeURIComponent(projectId)}/workflow`,
|
|
42
|
+
body: { nodeSchema: { ...(schema ?? {}), nodes: [...nodes, (0, exports.newShotNode)()], edges: Array.isArray(schema?.edges) ? schema.edges : [] } },
|
|
43
|
+
});
|
|
44
|
+
if (answer.status >= 200 && answer.status < 300)
|
|
45
|
+
return null;
|
|
46
|
+
const message = answer.body?.message ?? answer.body?.error;
|
|
47
|
+
return `Couldn't add a production node to the project's workflow: ${typeof message === 'string' ? message : `StreamBy answered ${answer.status}`}`;
|
|
48
|
+
}
|
|
@@ -38,6 +38,7 @@ const production_1 = require("./routes/production");
|
|
|
38
38
|
const rights_1 = require("./routes/rights");
|
|
39
39
|
const workflow_1 = require("./routes/workflow");
|
|
40
40
|
const pipeline_1 = require("./routes/pipeline");
|
|
41
|
+
const deliverable_1 = require("./routes/deliverable");
|
|
41
42
|
function createStreamByRouter(config) {
|
|
42
43
|
const router = express_1.default.Router();
|
|
43
44
|
if (config.encrypt)
|
|
@@ -134,5 +135,6 @@ function createStreamByRouter(config) {
|
|
|
134
135
|
router.use((0, rights_1.rightsRouter)(config));
|
|
135
136
|
router.use((0, workflow_1.workflowRouter)(config));
|
|
136
137
|
router.use((0, pipeline_1.pipelineRouter)(config));
|
|
138
|
+
router.use((0, deliverable_1.deliverableRouter)(config));
|
|
137
139
|
return router;
|
|
138
140
|
}
|
|
@@ -5,25 +5,34 @@ const express_1 = require("express");
|
|
|
5
5
|
const manager_1 = require("../../models/manager");
|
|
6
6
|
const auth_1 = require("../../utils/auth");
|
|
7
7
|
const apiTokens_1 = require("../../services/apiTokens");
|
|
8
|
-
//
|
|
9
|
-
// with a
|
|
8
|
+
// API tokens (see services/apiTokens), managed only with a session: no route here is
|
|
9
|
+
// reachable with a token (see apiTokenPermissions).
|
|
10
|
+
//
|
|
11
|
+
// - A user's own: listed, made and revoked by them.
|
|
12
|
+
// - A project's: listed, made and revoked by its admins only.
|
|
10
13
|
function apiTokenRouter(config) {
|
|
11
14
|
const router = (0, express_1.Router)();
|
|
12
15
|
const Project = (0, manager_1.getModel)('projects');
|
|
16
|
+
const failed = (res, what, err) => {
|
|
17
|
+
if (err instanceof apiTokens_1.ApiTokenError)
|
|
18
|
+
return res.status(err.status).json({ message: err.message });
|
|
19
|
+
res.status(500).json({ message: `Failed to ${what}`, details: err.message });
|
|
20
|
+
};
|
|
21
|
+
// The token itself is in the response that makes it only: it's never stored, nor shown again.
|
|
22
|
+
const created = (res, token, info) => res.status(201).json({ token, apiToken: info, message: 'Copy the token now: it will not be shown again' });
|
|
13
23
|
router.get('/user/api-tokens', async (req, res) => {
|
|
14
24
|
try {
|
|
15
25
|
const auth = req.auth;
|
|
16
|
-
res.json({ apiTokens: await (0, apiTokens_1.
|
|
26
|
+
res.json({ apiTokens: await (0, apiTokens_1.listUserApiTokens)(auth.userId) });
|
|
17
27
|
}
|
|
18
28
|
catch (err) {
|
|
19
|
-
res
|
|
29
|
+
failed(res, 'list API tokens', err);
|
|
20
30
|
}
|
|
21
31
|
});
|
|
22
|
-
// The token itself is in this response only: it's never stored, nor shown again.
|
|
23
32
|
router.post('/user/api-tokens', async (req, res) => {
|
|
24
33
|
try {
|
|
25
34
|
const auth = req.auth;
|
|
26
|
-
const { name,
|
|
35
|
+
const { name, permissions, projectIds, expiresInDays } = req.body ?? {};
|
|
27
36
|
// A token reaches only projects its owner is in.
|
|
28
37
|
if (Array.isArray(projectIds)) {
|
|
29
38
|
for (const id of projectIds) {
|
|
@@ -33,25 +42,79 @@ function apiTokenRouter(config) {
|
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
|
-
const { token, info } = await (0, apiTokens_1.createApiToken)(auth.userId, { name,
|
|
37
|
-
res
|
|
45
|
+
const { token, info } = await (0, apiTokens_1.createApiToken)({ kind: 'user', userId: auth.userId, username: auth.username }, { name, permissions, projectIds, expiresInDays: expiresInDays ?? null }, config);
|
|
46
|
+
created(res, token, info);
|
|
38
47
|
}
|
|
39
48
|
catch (err) {
|
|
40
|
-
|
|
41
|
-
return res.status(err.status).json({ message: err.message });
|
|
42
|
-
res.status(500).json({ message: 'Failed to create API token', details: err.message });
|
|
49
|
+
failed(res, 'create API token', err);
|
|
43
50
|
}
|
|
44
51
|
});
|
|
45
52
|
router.delete('/user/api-tokens/:tokenId', async (req, res) => {
|
|
46
53
|
try {
|
|
47
54
|
const auth = req.auth;
|
|
48
|
-
|
|
49
|
-
if (!revoked)
|
|
55
|
+
if (!(await (0, apiTokens_1.revokeUserApiToken)(auth.userId, req.params.tokenId))) {
|
|
50
56
|
return res.status(404).json({ message: 'API token not found' });
|
|
57
|
+
}
|
|
58
|
+
res.status(204).end();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
failed(res, 'revoke API token', err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// ── A project's tokens: its admins only ──
|
|
65
|
+
const adminsProject = async (req, res) => {
|
|
66
|
+
const auth = req.auth;
|
|
67
|
+
const project = await Project.findOne({ _id: req.params.id });
|
|
68
|
+
if (!project) {
|
|
69
|
+
res.status(404).json({ message: 'Project not found' });
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!(0, auth_1.isProjectAdmin)(project, auth.userId)) {
|
|
73
|
+
res.status(403).json({ message: "Only a project admin manages the project's API tokens" });
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return project;
|
|
77
|
+
};
|
|
78
|
+
router.get('/projects/:id/api-tokens', async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const project = await adminsProject(req, res);
|
|
81
|
+
if (!project)
|
|
82
|
+
return;
|
|
83
|
+
// Whether whoever made each one is still in the project: it keeps working if they aren't.
|
|
84
|
+
const apiTokens = (await (0, apiTokens_1.listProjectApiTokens)(req.params.id)).map(token => ({
|
|
85
|
+
...token,
|
|
86
|
+
createdBy: { ...token.createdBy, isMember: !!(0, auth_1.activeMemberOf)(project, token.createdBy.userId) },
|
|
87
|
+
}));
|
|
88
|
+
res.json({ apiTokens });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
failed(res, "list the project's API tokens", err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
router.post('/projects/:id/api-tokens', async (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const auth = req.auth;
|
|
97
|
+
if (!(await adminsProject(req, res)))
|
|
98
|
+
return;
|
|
99
|
+
const { name, permissions, expiresInDays } = req.body ?? {};
|
|
100
|
+
const { token, info } = await (0, apiTokens_1.createApiToken)({ kind: 'project', userId: auth.userId, username: auth.username, projectId: req.params.id }, { name, permissions, expiresInDays: expiresInDays ?? null }, config);
|
|
101
|
+
created(res, token, info);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
failed(res, "create the project's API token", err);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
router.delete('/projects/:id/api-tokens/:tokenId', async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
if (!(await adminsProject(req, res)))
|
|
110
|
+
return;
|
|
111
|
+
if (!(await (0, apiTokens_1.revokeProjectApiToken)(req.params.id, req.params.tokenId))) {
|
|
112
|
+
return res.status(404).json({ message: 'API token not found' });
|
|
113
|
+
}
|
|
51
114
|
res.status(204).end();
|
|
52
115
|
}
|
|
53
116
|
catch (err) {
|
|
54
|
-
res
|
|
117
|
+
failed(res, "revoke the project's API token", err);
|
|
55
118
|
}
|
|
56
119
|
});
|
|
57
120
|
return router;
|