streamby-core 0.35.0 → 0.37.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.
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOOLS = void 0;
4
+ const zod_1 = require("zod");
5
+ const tool = (definition) => definition;
6
+ const id = (what) => zod_1.z.string().min(1).describe(`The ${what}'s id`);
7
+ const projectId = id('project');
8
+ const enc = encodeURIComponent;
9
+ const byProject = ({ projectId }) => projectId;
10
+ const CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
11
+ const shotFields = {
12
+ 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"),
15
+ assetId: zod_1.z.string().optional(),
16
+ exportId: zod_1.z.string().optional(),
17
+ dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
18
+ order: zod_1.z.number().optional(),
19
+ };
20
+ 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"),
24
+ dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
25
+ notes: zod_1.z.string().optional(),
26
+ };
27
+ const rightsFields = {
28
+ rightsHolder: zod_1.z.string().optional(),
29
+ licenseType: zod_1.z.string().optional().describe('e.g. CC0, CC BY 4.0, proprietary'),
30
+ licenseUrl: zod_1.z.string().optional(),
31
+ territory: zod_1.z.string().optional(),
32
+ expiresAt: zod_1.z.string().optional().describe('An ISO 8601 date'),
33
+ usageRestrictions: zod_1.z.string().optional(),
34
+ notes: zod_1.z.string().optional(),
35
+ };
36
+ // Only the fields given (an unset optional field isn't sent as undefined).
37
+ const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
38
+ exports.TOOLS = [
39
+ // ── Reading ──
40
+ tool({
41
+ name: 'list_projects', title: 'List projects', permissions: [],
42
+ description: 'The StreamBy projects this token reaches.',
43
+ input: {},
44
+ call: () => ({ method: 'GET', path: '/projects' }),
45
+ pick: (data) => data.projects,
46
+ }),
47
+ tool({
48
+ name: 'get_project', title: 'Get a project', permissions: ['project:read'],
49
+ description: "A project's details: its name, description, members, exports and connections.",
50
+ input: { projectId },
51
+ projectOf: byProject,
52
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}` }),
53
+ }),
54
+ tool({
55
+ name: 'list_assets', title: 'List assets', permissions: ['assets:read'],
56
+ 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
+ input: {
58
+ projectId,
59
+ category: zod_1.z.enum(CATEGORIES),
60
+ connectionId: zod_1.z.string().optional().describe("A storage connection's id; the project's built-in storage if not given"),
61
+ query: zod_1.z.string().optional().describe('Only assets whose name contains it (case-insensitive)'),
62
+ },
63
+ projectOf: byProject,
64
+ call: ({ projectId, category, connectionId }) => ({
65
+ method: 'GET',
66
+ path: connectionId
67
+ ? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}/files/${category}`
68
+ : `/projects/${enc(projectId)}/storage/${category}`,
69
+ }),
70
+ pick: (data, { query }) => {
71
+ const files = Array.isArray(data?.data) ? data.data : [];
72
+ if (!query)
73
+ return files;
74
+ const q = query.toLowerCase();
75
+ return files.filter(f => [f.name, f.displayName, f.originalName, f.fileName]
76
+ .some(n => typeof n === 'string' && n.toLowerCase().includes(q)));
77
+ },
78
+ }),
79
+ tool({
80
+ name: 'get_asset_metadata', title: "Get an asset's metadata", permissions: ['assets:read'],
81
+ description: "An asset's metadata (technical data and custom tags).",
82
+ input: { projectId, fileId: id('asset') },
83
+ projectOf: byProject,
84
+ call: ({ projectId, fileId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata` }),
85
+ }),
86
+ tool({
87
+ name: 'get_asset_rights', title: "Get an asset's rights", permissions: ['rights:read'],
88
+ description: "An asset's rights: holder, license, territory, expiry and restrictions.",
89
+ input: { projectId, assetId: id('asset') },
90
+ projectOf: byProject,
91
+ call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights` }),
92
+ }),
93
+ tool({
94
+ name: 'get_workflow', title: "Get a project's workflow", permissions: ['workflow:read'],
95
+ description: "A project's workflow (its node graph).",
96
+ input: { projectId },
97
+ projectOf: byProject,
98
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/workflow` }),
99
+ }),
100
+ tool({
101
+ name: 'list_pipelines', title: 'List pipelines', permissions: ['workflow:read'],
102
+ description: "A project's pipelines.",
103
+ input: { projectId },
104
+ projectOf: byProject,
105
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines` }),
106
+ }),
107
+ tool({
108
+ name: 'get_pipeline', title: 'Get a pipeline', permissions: ['workflow:read'],
109
+ description: 'A pipeline, with its node schema.',
110
+ input: { projectId, pipelineId: id('pipeline') },
111
+ projectOf: byProject,
112
+ call: ({ projectId, pipelineId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}` }),
113
+ }),
114
+ tool({
115
+ name: 'list_exports', title: 'List exports', permissions: ['project:read', 'exports:read'],
116
+ description: "A project's exports (its published endpoints).",
117
+ input: { projectId },
118
+ projectOf: byProject,
119
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}` }),
120
+ pick: (data) => data?.project?.exports ?? data?.exports ?? [],
121
+ }),
122
+ tool({
123
+ name: 'get_export', title: 'Get an export', permissions: ['exports:read'],
124
+ description: 'An export, with its node schema.',
125
+ input: { projectId, exportId: id('export') },
126
+ projectOf: byProject,
127
+ call: ({ projectId, exportId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/exports/${enc(exportId)}` }),
128
+ }),
129
+ tool({
130
+ name: 'get_job', title: 'Get a job', permissions: ['jobs:read'],
131
+ description: "A job's status and progress (transcoding, rendering, ingesting...).",
132
+ input: { jobId: id('job') },
133
+ call: ({ jobId }) => ({ method: 'GET', path: `/jobs/${enc(jobId)}` }),
134
+ }),
135
+ tool({
136
+ name: 'list_sequences', title: 'List production sequences', permissions: ['production:read'],
137
+ description: "A project's production sequences.",
138
+ input: { projectId },
139
+ projectOf: byProject,
140
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences` }),
141
+ }),
142
+ tool({
143
+ name: 'list_shots', title: 'List shots', permissions: ['production:read'],
144
+ description: "A production sequence's shots.",
145
+ input: { projectId, sequenceId: id('sequence') },
146
+ projectOf: byProject,
147
+ call: ({ projectId, sequenceId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots` }),
148
+ }),
149
+ tool({
150
+ name: 'list_tasks', title: 'List shot tasks', permissions: ['production:read'],
151
+ description: "A shot's tasks.",
152
+ input: { projectId, shotId: id('shot') },
153
+ projectOf: byProject,
154
+ call: ({ projectId, shotId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks` }),
155
+ }),
156
+ // ── Writing ──
157
+ tool({
158
+ name: 'update_asset_metadata', title: "Update an asset's custom tags", permissions: ['assets:write'], write: true,
159
+ description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had.",
160
+ input: { projectId, fileId: id('asset'), customTags: zod_1.z.record(zod_1.z.string(), zod_1.z.string()) },
161
+ projectOf: byProject,
162
+ call: ({ projectId, fileId, customTags }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata`, body: { customTags } }),
163
+ }),
164
+ tool({
165
+ name: 'set_asset_rights', title: "Set an asset's rights", permissions: ['rights:write'], write: true,
166
+ description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes.",
167
+ input: { projectId, assetId: id('asset'), ...rightsFields },
168
+ projectOf: byProject,
169
+ call: ({ projectId, assetId, ...rights }) => ({ method: 'PUT', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights`, body: defined(rights) }),
170
+ }),
171
+ tool({
172
+ name: 'create_pipeline', title: 'Create a pipeline', permissions: ['workflow:write'], write: true,
173
+ description: 'Creates a pipeline in a project.',
174
+ input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
175
+ projectOf: byProject,
176
+ call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/pipelines`, body: defined(fields) }),
177
+ }),
178
+ tool({
179
+ name: 'update_pipeline', title: 'Update a pipeline', permissions: ['workflow:write'], write: true,
180
+ description: "Changes a pipeline's name, description or order.",
181
+ 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
+ projectOf: byProject,
183
+ call: ({ projectId, pipelineId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}`, body: defined(fields) }),
184
+ }),
185
+ tool({
186
+ name: 'create_sequence', title: 'Create a production sequence', permissions: ['production:write'], write: true,
187
+ description: 'Creates a production sequence in a project.',
188
+ input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
189
+ projectOf: byProject,
190
+ call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences`, body: defined(fields) }),
191
+ }),
192
+ tool({
193
+ name: 'update_sequence', title: 'Update a production sequence', permissions: ['production:write'], write: true,
194
+ description: "Changes a sequence's name, description or order.",
195
+ 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
+ projectOf: byProject,
197
+ call: ({ projectId, sequenceId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}`, body: defined(fields) }),
198
+ }),
199
+ tool({
200
+ name: 'create_shot', title: 'Create a shot', permissions: ['production:write'], write: true,
201
+ description: 'Creates a shot in a production sequence.',
202
+ input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1), ...shotFields },
203
+ projectOf: byProject,
204
+ call: ({ projectId, sequenceId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots`, body: defined(fields) }),
205
+ }),
206
+ tool({
207
+ name: 'update_shot', title: 'Update a shot', permissions: ['production:write'], write: true,
208
+ description: 'Changes a shot (its name, status, assignee, due date...).',
209
+ input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1).optional(), ...shotFields },
210
+ projectOf: byProject,
211
+ call: ({ projectId, shotId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}`, body: defined(fields) }),
212
+ }),
213
+ tool({
214
+ name: 'create_task', title: 'Create a shot task', permissions: ['production:write'], write: true,
215
+ description: 'Creates a task in a shot.',
216
+ input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1), ...taskFields },
217
+ projectOf: byProject,
218
+ call: ({ projectId, shotId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks`, body: defined(fields) }),
219
+ }),
220
+ tool({
221
+ name: 'update_task', title: 'Update a shot task', permissions: ['production:write'], write: true,
222
+ description: 'Changes a task (its name, status, priority, assignee, due date, notes).',
223
+ input: { projectId, shotId: id('shot'), taskId: id('task'), name: zod_1.z.string().min(1).optional(), ...taskFields },
224
+ projectOf: byProject,
225
+ call: ({ projectId, shotId, taskId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}`, body: defined(fields) }),
226
+ }),
227
+ // ── Projects ──
228
+ tool({
229
+ name: 'create_project', title: 'Create a project', permissions: ['projects:create'], write: true,
230
+ description: 'Creates a StreamBy project, its maker being its admin, as from "New project" in the UI. The token reaches it from then on.',
231
+ input: {
232
+ name: zod_1.z.string().min(1),
233
+ description: zod_1.z.string().optional(),
234
+ public: zod_1.z.boolean().optional().describe('Whether it shows in Explore (true if not given; a private one needs a subscriber plan)'),
235
+ category: zod_1.z.string().optional(),
236
+ },
237
+ call: (fields) => ({ method: 'POST', path: '/projects/create', body: defined(fields) }),
238
+ pick: (data) => ({ projectId: data?.projectId, message: data?.message }),
239
+ }),
240
+ tool({
241
+ name: 'delete_project', title: 'Delete a project', permissions: ['project:delete'], write: true, destructive: true,
242
+ description: 'Deletes a project, with everything in it, for every member. It cannot be undone.',
243
+ input: { projectId },
244
+ projectOf: byProject,
245
+ call: ({ projectId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}` }),
246
+ pick: (data) => ({ message: data?.message }),
247
+ }),
248
+ // ── Deleting ──
249
+ tool({
250
+ name: 'delete_asset', title: 'Delete an asset', permissions: ['assets:delete'], write: true, destructive: true,
251
+ description: "Deletes an asset's file, from the project's built-in storage or from one of its storage connections. It cannot be undone.",
252
+ input: {
253
+ projectId,
254
+ fileId: id('asset'),
255
+ connectionId: zod_1.z.string().optional().describe("The storage connection it's in; the project's built-in storage if not given"),
256
+ },
257
+ projectOf: byProject,
258
+ call: ({ projectId, fileId, connectionId }) => ({
259
+ method: 'DELETE',
260
+ path: connectionId
261
+ ? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}/files/${enc(fileId)}`
262
+ : `/projects/${enc(projectId)}/storage/files/${enc(fileId)}`,
263
+ }),
264
+ }),
265
+ tool({
266
+ name: 'delete_sequence', title: 'Delete a production sequence', permissions: ['production:delete'], write: true, destructive: true,
267
+ description: 'Deletes a production sequence, with its shots. It cannot be undone.',
268
+ input: { projectId, sequenceId: id('sequence') },
269
+ projectOf: byProject,
270
+ call: ({ projectId, sequenceId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}` }),
271
+ }),
272
+ tool({
273
+ name: 'delete_shot', title: 'Delete a shot', permissions: ['production:delete'], write: true, destructive: true,
274
+ description: 'Deletes a shot, with its tasks. It cannot be undone.',
275
+ input: { projectId, shotId: id('shot') },
276
+ projectOf: byProject,
277
+ call: ({ projectId, shotId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}` }),
278
+ }),
279
+ tool({
280
+ name: 'delete_task', title: 'Delete a shot task', permissions: ['production:delete'], write: true, destructive: true,
281
+ description: "Deletes a shot's task. It cannot be undone.",
282
+ input: { projectId, shotId: id('shot'), taskId: id('task') },
283
+ projectOf: byProject,
284
+ call: ({ projectId, shotId, taskId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}` }),
285
+ }),
286
+ ];
@@ -13,6 +13,7 @@ const reencryptCredentials_1 = require("../services/reencryptCredentials");
13
13
  const wsHub_1 = require("../services/wsHub");
14
14
  const auth_1 = require("../services/auth");
15
15
  const apiToken_1 = require("./routes/apiToken");
16
+ const mcp_1 = require("./routes/mcp");
16
17
  const encryption_1 = require("../utils/encryption");
17
18
  const auth_2 = require("./routes/auth");
18
19
  const user_1 = require("./routes/user");
@@ -97,6 +98,7 @@ function createStreamByRouter(config) {
97
98
  (0, manager_1.registerModel)('user_subscriptions', [mainDb.id], 'user_subscriptions', 'streamby');
98
99
  (0, manager_1.registerModel)('user_integrations', [mainDb.id], 'user_integrations', streambySchema);
99
100
  (0, manager_1.registerModel)('api_tokens', [mainDb.id], 'api_tokens', streambySchema);
101
+ (0, manager_1.registerModel)('mcp_audit', [mainDb.id], 'mcp_audit', streambySchema);
100
102
  }
101
103
  }
102
104
  if (config.websocket?.server) {
@@ -110,6 +112,7 @@ function createStreamByRouter(config) {
110
112
  router.use(subscription_1.ensureSubscription);
111
113
  router.use((0, user_1.userRouter)(config));
112
114
  router.use((0, apiToken_1.apiTokenRouter)(config));
115
+ router.use((0, mcp_1.mcpRouter)(config));
113
116
  router.use((0, export_1.exportRouter)(config));
114
117
  router.use((0, member_1.memberRouter)(config));
115
118
  router.use((0, storage_2.storageRouter)(config));
@@ -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 mcpRouter(config: StreamByConfig): Router;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mcpRouter = mcpRouter;
4
+ const express_1 = require("express");
5
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
6
+ const server_1 = require("../../mcp/server");
7
+ const rateLimit_1 = require("../../mcp/rateLimit");
8
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
9
+ const { version } = require('../../../package.json');
10
+ const jsonRpcError = (res, status, message) => res.status(status).json({ jsonrpc: '2.0', error: { code: -32000, message }, id: null });
11
+ // StreamBy's MCP server (Streamable HTTP, stateless: one server per request), for agents
12
+ // working with StreamBy through an API token. Each tool calls the REST API right here -- this
13
+ // same process, under the same mount -- with the request's own Authorization header, so it
14
+ // reaches what the token and its owner can and nothing more.
15
+ function mcpRouter(config) {
16
+ const router = (0, express_1.Router)();
17
+ const limiter = (0, rateLimit_1.createRateLimiter)(config.mcp?.rateLimit ?? rateLimit_1.DEFAULT_MCP_RATE_LIMIT);
18
+ router.post('/mcp', async (req, res) => {
19
+ const auth = req.auth;
20
+ if (!auth.apiToken)
21
+ return jsonRpcError(res, 401, 'The MCP server takes an API token (Authorization: Bearer sbt_...)');
22
+ const taken = limiter.take(auth.apiToken.tokenId);
23
+ if (!taken.allowed) {
24
+ res.setHeader('Retry-After', String(Math.ceil(taken.retryAfterMs / 1000)));
25
+ return jsonRpcError(res, 429, 'Too many requests for this API token; try again later');
26
+ }
27
+ const base = config.mcp?.apiBaseUrl ?? `http://127.0.0.1:${req.socket.localPort}${req.baseUrl}`;
28
+ const api = async ({ method, path, body }) => {
29
+ const answer = await fetch(`${base}${path}`, {
30
+ method,
31
+ headers: { Authorization: req.headers.authorization ?? '', ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
32
+ body: body !== undefined ? JSON.stringify(body) : undefined,
33
+ redirect: 'error',
34
+ });
35
+ const text = await answer.text();
36
+ let parsed = null;
37
+ try {
38
+ parsed = text ? JSON.parse(text) : null;
39
+ }
40
+ catch {
41
+ parsed = text;
42
+ }
43
+ return { status: answer.status, body: parsed };
44
+ };
45
+ const server = (0, server_1.createMcpServer)(auth, api, config, version);
46
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
47
+ res.on('close', () => { transport.close(); server.close(); });
48
+ try {
49
+ await server.connect(transport);
50
+ await transport.handleRequest(req, res, req.body);
51
+ }
52
+ catch (err) {
53
+ console.error('❌ StreamBy MCP: request failed:', err instanceof Error ? err.message : err);
54
+ if (!res.headersSent)
55
+ jsonRpcError(res, 500, 'Internal error');
56
+ }
57
+ });
58
+ // Stateless: no session to stream to, nor to end.
59
+ router.get('/mcp', (_req, res) => jsonRpcError(res, 405, 'Method not allowed'));
60
+ router.delete('/mcp', (_req, res) => jsonRpcError(res, 405, 'Method not allowed'));
61
+ return router;
62
+ }
@@ -7,6 +7,7 @@ const auth_1 = require("../../utils/auth");
7
7
  const connectionManager_1 = require("../../adapters/database/connectionManager");
8
8
  const sql_1 = require("../../adapters/database/sql");
9
9
  const notification_1 = require("../../services/notification");
10
+ const apiTokens_1 = require("../../services/apiTokens");
10
11
  const VALID_ROLES = ['viewer', 'editor', 'admin'];
11
12
  function memberRouter(config) {
12
13
  const router = (0, express_1.Router)();
@@ -302,6 +303,13 @@ function memberRouter(config) {
302
303
  else {
303
304
  await Project.update({ _id: projectId }, { $pull: { members: { userId: targetUserId } } });
304
305
  }
306
+ // A project's tokens they made keep working: its admins should know.
307
+ const tokens = await (0, apiTokens_1.projectApiTokensMadeBy)(projectId, targetUserId);
308
+ if (tokens.length) {
309
+ const names = tokens.map(t => `"${t.name}"`).join(', ');
310
+ const admins = (project.members ?? []).filter((m) => m.role === 'admin' && m.status === 'active' && m.userId?.toString() !== targetUserId?.toString());
311
+ await Promise.all(admins.map((admin) => (0, notification_1.createNotification)(admin.userId.toString(), 'api_token_creator_left', `${tokens.length === 1 ? 'An API token' : `${tokens.length} API tokens`} of "${project.name}" (${names}) made by a member who left keep working until revoked`, { projectId, tokenIds: tokens.map(t => t.id) }).catch(() => { })));
312
+ }
305
313
  res.status(200).json({ message: 'Member removed successfully' });
306
314
  }
307
315
  catch (err) {
@@ -9,6 +9,7 @@ const sql_1 = require("../../adapters/database/sql");
9
9
  const sanitize_1 = require("../../utils/sanitize");
10
10
  const projectIntegration_1 = require("../../services/projectIntegration");
11
11
  const auth_2 = require("../../services/auth");
12
+ const apiTokens_1 = require("../../services/apiTokens");
12
13
  function projectRouter(config) {
13
14
  const router = (0, express_1.Router)();
14
15
  const Project = (0, manager_1.getModel)('projects');
@@ -30,8 +31,8 @@ function projectRouter(config) {
30
31
  const allProjects = await Project.find({}); // Fetch all projects
31
32
  const projects = allProjects
32
33
  .filter(project => {
33
- const currentUserMember = project.members?.find((m) => m.userId?.toString() === auth.userId?.toString());
34
- if (!currentUserMember || currentUserMember.status !== 'active')
34
+ const currentUserMember = (0, auth_1.activeMemberOf)(project, auth.userId?.toString());
35
+ if (!currentUserMember)
35
36
  return false;
36
37
  // With an API token, only the projects it reaches.
37
38
  if (!(0, auth_2.scopeReachesProject)(auth.apiToken, (project._id ?? project.id)?.toString()))
@@ -122,6 +123,9 @@ function projectRouter(config) {
122
123
  integrationConnections: [],
123
124
  });
124
125
  const newProjectId = newProject._id || newProject.id;
126
+ // A token that made it reaches it.
127
+ if (auth.apiToken?.kind === 'user')
128
+ await (0, apiTokens_1.addProjectToApiToken)(auth.apiToken.tokenId, String(newProjectId));
125
129
  if (dbConnections.length || storageConnections.length || integrationConnections.length) {
126
130
  const stamped = await Project.update({ _id: newProjectId }, {
127
131
  dbConnections: dbConnections.map(c => ({ ...c, projectId: newProjectId })),
@@ -253,6 +257,8 @@ function projectRouter(config) {
253
257
  return res.status(403).json({ message: 'Only a project admin can delete it' });
254
258
  }
255
259
  await Project.useDbType(project.dbType).delete({ _id: projectId });
260
+ // Its tokens have nothing left to reach.
261
+ await (0, apiTokens_1.deleteProjectApiTokens)(String(projectId));
256
262
  const allProjects = await Project.find({});
257
263
  const projects = allProjects
258
264
  .filter(project => {
@@ -18,6 +18,9 @@ const ensureSubscription = async (req, res, next) => {
18
18
  const auth = req.auth;
19
19
  if (!auth)
20
20
  return next();
21
+ // A project's token is no user: it has no plan of its own.
22
+ if (auth.apiToken?.kind === 'project')
23
+ return next();
21
24
  const plan = await resolveSubscription(auth);
22
25
  req.subscription = plan;
23
26
  next();
@@ -0,0 +1,16 @@
1
+ /** Permissions on a project, for the projects a token reaches. */
2
+ export declare const PROJECT_PERMISSIONS: readonly ["project:read", "project:update", "project:delete", "members:read", "members:manage", "assets:read", "assets:write", "assets:delete", "production:read", "production:write", "production:delete", "workflow:read", "workflow:write", "exports:read", "exports:write", "rights:read", "rights:write", "jobs:read", "jobs:run"];
3
+ /** Permissions on the account: only a user's token has them. */
4
+ export declare const ACCOUNT_PERMISSIONS: readonly ["projects:create"];
5
+ export type ProjectPermission = typeof PROJECT_PERMISSIONS[number];
6
+ export type AccountPermission = typeof ACCOUNT_PERMISSIONS[number];
7
+ export type ApiTokenPermission = ProjectPermission | AccountPermission;
8
+ export declare const isProjectPermission: (value: unknown) => value is ProjectPermission;
9
+ export declare const isAccountPermission: (value: unknown) => value is AccountPermission;
10
+ /** What a route needs of a token: a permission, or only being a token (`null`). */
11
+ export interface RouteRule {
12
+ permission: ApiTokenPermission | null;
13
+ userTokenOnly?: boolean;
14
+ }
15
+ /** The rule for a request made with an API token, or undefined: not reachable with one. */
16
+ export declare function routeRuleFor(method: string, path: string): RouteRule | undefined;