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.
Files changed (46) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +141 -44
  3. package/dist/adapters/database/connectionManager.js +24 -11
  4. package/dist/adapters/storage/s3.d.ts +3 -2
  5. package/dist/adapters/storage/s3.js +18 -5
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.js +4 -1
  8. package/dist/mcp/server.d.ts +4 -3
  9. package/dist/mcp/server.js +25 -10
  10. package/dist/mcp/tools.d.ts +7 -2
  11. package/dist/mcp/tools.js +187 -35
  12. package/dist/mcp/workflowNodes.d.ts +24 -0
  13. package/dist/mcp/workflowNodes.js +48 -0
  14. package/dist/middleware/createRouter.js +2 -0
  15. package/dist/middleware/routes/apiToken.js +77 -14
  16. package/dist/middleware/routes/deliverable.d.ts +3 -0
  17. package/dist/middleware/routes/deliverable.js +131 -0
  18. package/dist/middleware/routes/export.js +2 -0
  19. package/dist/middleware/routes/member.js +8 -0
  20. package/dist/middleware/routes/production.js +60 -67
  21. package/dist/middleware/routes/project.js +15 -3
  22. package/dist/middleware/routes/storage.js +115 -24
  23. package/dist/middleware/routes/subscription.js +3 -0
  24. package/dist/services/apiTokenPermissions.d.ts +16 -0
  25. package/dist/services/apiTokenPermissions.js +102 -0
  26. package/dist/services/apiTokens.d.ts +38 -14
  27. package/dist/services/apiTokens.js +134 -40
  28. package/dist/services/assetVersions.d.ts +48 -0
  29. package/dist/services/assetVersions.js +84 -0
  30. package/dist/services/auth.d.ts +4 -1
  31. package/dist/services/auth.js +24 -18
  32. package/dist/services/deliverables.d.ts +26 -0
  33. package/dist/services/deliverables.js +55 -0
  34. package/dist/services/mediaProcessor.js +3 -1
  35. package/dist/services/production.d.ts +2 -0
  36. package/dist/services/production.js +13 -0
  37. package/dist/services/productionFields.d.ts +16 -0
  38. package/dist/services/productionFields.js +80 -0
  39. package/dist/services/signedUrls.d.ts +6 -0
  40. package/dist/services/signedUrls.js +9 -0
  41. package/dist/types/index.d.ts +12 -4
  42. package/dist/utils/auth.d.ts +12 -1
  43. package/dist/utils/auth.js +23 -2
  44. package/dist/utils/nosqlDb.d.ts +2 -0
  45. package/dist/utils/nosqlDb.js +11 -0
  46. 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.string().optional().describe('Its status, as the project uses them'),
14
- assignedTo: zod_1.z.string().optional().describe("The assignee's user id"),
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.string().optional(),
22
- priority: zod_1.z.string().optional(),
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', write: false,
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', write: false,
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', write: false,
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", write: false,
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", write: false,
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", write: false,
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', write: false,
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', write: false,
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', write: false,
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', write: false,
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', write: false,
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', write: false,
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', write: false,
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', write: false,
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: 'Creates a pipeline in a project.',
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: 'Creates a production sequence in a project.',
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: 'Creates a shot in a production sequence.',
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: 'Creates a task in a shot.',
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
- // The signed-in user's own API tokens (see services/apiTokens): listed, made, revoked. Only
9
- // with a session: a token can't manage tokens (see enforceApiTokenScope).
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.listApiTokens)(auth.userId) });
26
+ res.json({ apiTokens: await (0, apiTokens_1.listUserApiTokens)(auth.userId) });
17
27
  }
18
28
  catch (err) {
19
- res.status(500).json({ message: 'Failed to list API tokens', details: err.message });
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, access, projectIds, expiresInDays } = req.body ?? {};
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, access, projectIds, expiresInDays: expiresInDays ?? null }, config);
37
- res.status(201).json({ token, apiToken: info, message: 'Copy the token now: it will not be shown again' });
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
- if (err instanceof apiTokens_1.ApiTokenError)
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
- const revoked = await (0, apiTokens_1.revokeApiToken)(auth.userId, req.params.tokenId);
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.status(500).json({ message: 'Failed to revoke API token', details: err.message });
117
+ failed(res, "revoke the project's API token", err);
55
118
  }
56
119
  });
57
120
  return router;
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function deliverableRouter(_config: StreamByConfig): Router;