streamby-core 0.36.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.37.0
4
+
5
+ ### ⚠️ Breaking changes
6
+
7
+ - **API tokens have permissions, not an access level.** The tokens made before this version no
8
+ longer work, and aren't listed: make new ones. There's no migration. The `api_tokens` table
9
+ gets its new columns at startup (`kind`, `permissions`, `createdByName`), and its old
10
+ `access` column becomes optional; nothing else is changed.
11
+ - **`ApiTokenScope` changed.** It has `kind` (`'user'` or `'project'`) and `permissions`,
12
+ instead of `access`. `ApiTokenAccess` is gone.
13
+ - **`createApiToken`** takes who makes the token (`{ kind, userId, username, projectId }`), and
14
+ `permissions` instead of `access`. `listApiTokens` and `revokeApiToken` are now
15
+ `listUserApiTokens` and `revokeUserApiToken`.
16
+ - **The host's `authProvider` must handle a project's token** (see the README): it isn't a
17
+ user, so it must not be looked up as one. `resolveApiToken` returns its `name` to go by.
18
+
19
+ ### Added
20
+
21
+ - **Permissions** for tokens, by resource and action: `project`, `members`, `assets`,
22
+ `production`, `workflow`, `exports`, `rights` and `jobs`, each with `read` and its writes
23
+ (`update`, `write`, `manage`, `delete` or `run`), and `projects:create` for the account.
24
+ Every route a token reaches needs its own permission; a route no permission covers isn't
25
+ reachable with a token. A user's token is still limited by its user's role in each project.
26
+ `PROJECT_PERMISSIONS` and `ACCOUNT_PERMISSIONS` are exported.
27
+ - **Projects' tokens**, at `GET`, `POST` and `DELETE /projects/:id/api-tokens`, managed by the
28
+ project's admins only. They act on their project alone, with their permissions, and keep
29
+ working after whoever made them leaves it; the project's admins are notified when that
30
+ happens (`api_token_creator_left`). Deleting a project ends its tokens.
31
+ - **`projects:create`**: a user's token with it creates projects, and reaches the ones it
32
+ creates.
33
+ - **MCP tools** `create_project`, `delete_project`, `delete_asset`, `delete_sequence`,
34
+ `delete_shot` and `delete_task`. Each tool needs its permissions and is listed only for a
35
+ token that has them; the ones that delete are marked as destructive.
36
+
3
37
  ## 0.36.0
4
38
 
5
39
  ### Added
package/README.md CHANGED
@@ -102,20 +102,49 @@ createStreamByRouter({
102
102
 
103
103
  ## 🎫 API tokens
104
104
 
105
- Agents and services work with StreamBy as a user through API tokens, without the user's
106
- browser session. A user manages their own tokens (`GET`, `POST` and
107
- `DELETE /streamby/user/api-tokens`, only with a session). Each token has:
108
-
109
- - **an access level**: `read` (only reads) or `write` (reads and writes);
110
- - **the projects it reaches**: a list of project ids, or `'*'` for every project the user is a
111
- member of, including ones they join later;
112
- - **an expiry**: 7, 30, 90 or 365 days, or none.
113
-
114
- A token is `sbt_` followed by 32 random bytes. It's shown once, when it's made. Only its
115
- SHA-256 hash is stored, with a short prefix to tell it apart.
105
+ Agents and services work with StreamBy through API tokens, without a browser session. There
106
+ are two kinds:
107
+
108
+ - **A user's token** acts as its user. The user manages their own (`GET`, `POST` and
109
+ `DELETE /streamby/user/api-tokens`). It reaches a list of the user's projects, or `'*'` for
110
+ every project they're a member of, including ones they join later. It never does more than
111
+ its user can: their role in each project still applies.
112
+ - **A project's token** acts on its project alone, as the project's, not as whoever made it:
113
+ it keeps working after they leave the project, for permanent integrations (CI, services, the
114
+ project's agents). Only the project's admins list, make and revoke them (`GET`, `POST` and
115
+ `DELETE /streamby/projects/:id/api-tokens`), and each one records who made it. When they
116
+ leave, the project's admins are notified that it still works. Deleting the project ends
117
+ its tokens.
118
+
119
+ Tokens are managed only with a session: a token can't manage tokens.
120
+
121
+ Each token has a name, **its permissions**, and **an expiry** (7, 30, 90 or 365 days, or
122
+ none). It's `sbt_` followed by 32 random bytes, shown once, when it's made. Only its SHA-256
123
+ hash is stored, with a short prefix to tell it apart.
124
+
125
+ **Permissions**
126
+
127
+ | Resource | Permissions | Covers |
128
+ |---|---|---|
129
+ | Project | `project:read`, `project:update`, `project:delete` | The project, its settings and its connections (API, database, storage, distribution, render farm), integrations and allowed origins. |
130
+ | Members | `members:read`, `members:manage` | Listing members; inviting, changing and removing them, and searching users. |
131
+ | Assets | `assets:read`, `assets:write`, `assets:delete` | Files and folders, upload URLs, metadata, versions, reviews and annotations, QC reports. |
132
+ | Production | `production:read`, `production:write`, `production:delete` | Sequences, shots and tasks. |
133
+ | Workflow | `workflow:read`, `workflow:write` | The workflow and its pipelines. |
134
+ | Exports | `exports:read`, `exports:write` | The project's exports. |
135
+ | Rights | `rights:read`, `rights:write` | Assets' rights. |
136
+ | Jobs | `jobs:read`, `jobs:run` | Following jobs; starting them (transcoding, rendering, AI, publishing a deliverable). |
137
+ | Account | `projects:create` | Creating projects. A user's token only; the token reaches what it creates. |
138
+
139
+ `PROJECT_PERMISSIONS` and `ACCOUNT_PERMISSIONS` are exported. A route no permission covers
140
+ (the user's notifications, integrations and tokens, Explore, accepting an invitation) isn't
141
+ reachable with a token.
142
+
143
+ **Resolving a token**
116
144
 
117
145
  Requests send it as `Authorization: Bearer sbt_...`. Your `authProvider` resolves it with
118
- `resolveApiToken` and returns its owner, with the token's scope in `apiToken`:
146
+ `resolveApiToken`, and returns its `userId` with the token's scope in `apiToken`. A project's
147
+ token isn't a user: its `userId` is the token's own (on its project), and it goes by its name.
119
148
 
120
149
  ```ts
121
150
  import { createStreamByRouter, resolveApiToken, API_TOKEN_PREFIX } from 'streamby-core';
@@ -127,6 +156,9 @@ createStreamByRouter({
127
156
  if (bearer?.startsWith(API_TOKEN_PREFIX)) {
128
157
  const resolved = await resolveApiToken(bearer); // null once revoked or expired
129
158
  if (!resolved) throw new Error('Invalid API token');
159
+ if (resolved.scope.kind === 'project') {
160
+ return { userId: resolved.userId, username: resolved.name, role: 'viewer', apiToken: resolved.scope };
161
+ }
130
162
  const user = await findUser(resolved.userId); // your own user lookup
131
163
  return { userId: user.id, username: user.username, role: user.role, apiToken: resolved.scope };
132
164
  }
@@ -135,17 +167,9 @@ createStreamByRouter({
135
167
  });
136
168
  ```
137
169
 
138
- A request made with a token reaches only what the token's scope allows:
139
-
140
- - a `read` token can't write;
141
- - a project outside the token's projects answers 404, and the project list shows only the
142
- token's projects;
143
- - a token can't manage tokens;
144
- - outside a project, a token can only make a few reads (`/auth`, `/projects`, `/jobs/:jobId`,
145
- `/user/subscription`, `/users/search`).
146
-
147
- The owner's role in each project still applies on top of that. A token never does more than
148
- its owner can, and it stops reaching a project as soon as its owner leaves it.
170
+ A request made with a token goes through only when its route's permission is among the
171
+ token's (otherwise 403), and a project outside the token's projects answers 404; the project
172
+ list shows only the token's projects.
149
173
 
150
174
  ---
151
175
 
@@ -156,24 +180,34 @@ StreamBy serves an [MCP](https://modelcontextprotocol.io) server at `POST /strea
156
180
  projects with an API token (see above). It takes only an API token; a browser session gets 401.
157
181
 
158
182
  Each tool calls StreamBy's own REST API with the caller's token. So it reaches exactly what the
159
- token's scope and its owner's role in each project allow, and a write is saved just like one
160
- made from the UI, which shows it when it loads the project.
183
+ token allows (and, for a user's token, what its user's role in each project allows), and a
184
+ change is saved just like one made from the UI, which shows it when it loads the project.
161
185
 
162
186
  **Tools**
163
187
 
164
- - **Reading**, with any token:
165
- - `list_projects`, `get_project`;
166
- - `list_assets` (by category, optionally in one storage connection, and filtered by name),
167
- `get_asset_metadata`, `get_asset_rights`;
168
- - `get_workflow`, `list_pipelines`, `get_pipeline`, `list_exports`, `get_export`;
169
- - `get_job`;
170
- - `list_sequences`, `list_shots`, `list_tasks`.
171
- - **Writing**, listed only for a `write` token, and allowed only where its owner is an editor or
172
- an admin:
173
- - `update_asset_metadata`, `set_asset_rights`;
174
- - `create_pipeline`, `update_pipeline`;
175
- - `create_sequence`, `update_sequence`, `create_shot`, `update_shot`, `create_task`,
176
- `update_task`.
188
+ A token sees only the tools whose permissions it has.
189
+
190
+ | Tools | Permissions |
191
+ |---|---|
192
+ | `list_projects` | any token |
193
+ | `get_project` | `project:read` |
194
+ | `create_project` | `projects:create` |
195
+ | `delete_project` | `project:delete` |
196
+ | `list_assets` (by category, optionally in one storage connection, and filtered by name), `get_asset_metadata` | `assets:read` |
197
+ | `update_asset_metadata` | `assets:write` |
198
+ | `delete_asset` | `assets:delete` |
199
+ | `get_asset_rights` / `set_asset_rights` | `rights:read` / `rights:write` |
200
+ | `get_workflow`, `list_pipelines`, `get_pipeline` | `workflow:read` |
201
+ | `create_pipeline`, `update_pipeline` | `workflow:write` |
202
+ | `list_exports` | `project:read` and `exports:read` |
203
+ | `get_export` | `exports:read` |
204
+ | `get_job` | `jobs:read` |
205
+ | `list_sequences`, `list_shots`, `list_tasks` | `production:read` |
206
+ | `create_sequence`, `update_sequence`, `create_shot`, `update_shot`, `create_task`, `update_task` | `production:write` |
207
+ | `delete_sequence`, `delete_shot`, `delete_task` | `production:delete` |
208
+
209
+ The tools that delete are marked as destructive (`destructiveHint`), so a client can ask before
210
+ running them.
177
211
 
178
212
  **Limits and audit**
179
213
 
@@ -181,8 +215,9 @@ made from the UI, which shows it when it loads the project.
181
215
  answers 429 with `Retry-After`. Each instance counts its own requests. Change the limit with
182
216
  `mcp.rateLimit`.
183
217
  - Every tool call is recorded in the `mcp_audit` table, created at startup in the main database.
184
- Each row has the user, the token's id, the project, the tool, whether it went through, the
185
- status, the error and the duration. A row never includes the arguments or the token.
218
+ Each row has the user (a project's token: the token itself), the token's id, the project, the
219
+ tool, whether it went through, the status, the error and the duration. A row never includes
220
+ the arguments or the token.
186
221
 
187
222
  ```ts
188
223
  createStreamByRouter({
@@ -197,8 +232,9 @@ createStreamByRouter({
197
232
 
198
233
  ### Connecting from Claude Code
199
234
 
200
- Make an API token in StreamBy (Settings → Security → Personal API tokens), and keep it in an
201
- environment variable, never in a file:
235
+ Make an API token in StreamBy, with only the permissions the agent needs: your own
236
+ (Settings → Security → Personal API tokens), or a project's (in the project, Settings → API
237
+ Tokens). Keep it in an environment variable, never in a file:
202
238
 
203
239
  ```bash
204
240
  export STREAMBY_TOKEN=sbt_...
@@ -223,8 +259,8 @@ environment:
223
259
 
224
260
  Claude Desktop runs local (stdio) servers from `claude_desktop_config.json`. Reach StreamBy
225
261
  through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), passing the token in its
226
- `env`. This file holds it in plain text, so use a token with only the scope you need, and
227
- an expiry:
262
+ `env`. This file holds it in plain text, so use a token with only the permissions you need,
263
+ and an expiry:
228
264
 
229
265
  ```json
230
266
  {
@@ -149,21 +149,34 @@ const ensureTablesExist = async (pool) => {
149
149
  `);
150
150
  console.log('✅ "user_integrations" table ensured to exist.');
151
151
  // API tokens: only their hash is kept (see services/apiTokens). Main db only, like
152
- // user_integrations above.
152
+ // user_integrations above. A table from before tokens had a kind and permissions gets the
153
+ // new columns; its old tokens no longer work (they have no kind) and aren't listed.
153
154
  await pool.query(`
154
155
  CREATE TABLE IF NOT EXISTS streamby.api_tokens (
155
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
156
- "userId" VARCHAR(255) NOT NULL,
157
- name VARCHAR(255) NOT NULL,
158
- prefix VARCHAR(32) NOT NULL,
159
- hash CHAR(64) NOT NULL UNIQUE,
160
- access VARCHAR(10) NOT NULL,
161
- "projectIds" JSONB NOT NULL,
162
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
163
- "expiresAt" TIMESTAMP WITH TIME ZONE,
164
- "lastUsedAt" TIMESTAMP WITH TIME ZONE
156
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
157
+ kind VARCHAR(10),
158
+ "userId" VARCHAR(255) NOT NULL,
159
+ "createdByName" VARCHAR(255),
160
+ name VARCHAR(255) NOT NULL,
161
+ prefix VARCHAR(32) NOT NULL,
162
+ hash CHAR(64) NOT NULL UNIQUE,
163
+ permissions JSONB NOT NULL DEFAULT '[]',
164
+ "projectIds" JSONB NOT NULL,
165
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
166
+ "expiresAt" TIMESTAMP WITH TIME ZONE,
167
+ "lastUsedAt" TIMESTAMP WITH TIME ZONE
165
168
  );
169
+ ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS kind VARCHAR(10);
170
+ ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS "createdByName" VARCHAR(255);
171
+ ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]';
172
+ DO $$ BEGIN
173
+ IF EXISTS (SELECT 1 FROM information_schema.columns
174
+ WHERE table_schema = 'streamby' AND table_name = 'api_tokens' AND column_name = 'access') THEN
175
+ ALTER TABLE streamby.api_tokens ALTER COLUMN access DROP NOT NULL;
176
+ END IF;
177
+ END $$;
166
178
  CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
179
+ CREATE INDEX IF NOT EXISTS api_tokens_kind_idx ON streamby.api_tokens (kind);
167
180
  `);
168
181
  console.log('✅ "api_tokens" table ensured to exist.');
169
182
  // The MCP server's audit log: every tool call (see mcp/audit). Main db only.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createStreamByRouter } from './middleware/createRouter';
2
2
  export { emitToUser } from './services/wsHub';
3
3
  export { createNotification } from './services/notification';
4
- export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenAccess } from './types';
4
+ export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission } from './types';
5
5
  export { resolveApiToken, API_TOKEN_PREFIX } from './services/apiTokens';
6
+ export { PROJECT_PERMISSIONS, ACCOUNT_PERMISSIONS } from './services/apiTokenPermissions';
6
7
  export type { ResolvedApiToken } from './services/apiTokens';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
3
+ exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
4
4
  var createRouter_1 = require("./middleware/createRouter");
5
5
  Object.defineProperty(exports, "createStreamByRouter", { enumerable: true, get: function () { return createRouter_1.createStreamByRouter; } });
6
6
  var wsHub_1 = require("./services/wsHub");
@@ -10,3 +10,6 @@ Object.defineProperty(exports, "createNotification", { enumerable: true, get: fu
10
10
  var apiTokens_1 = require("./services/apiTokens");
11
11
  Object.defineProperty(exports, "resolveApiToken", { enumerable: true, get: function () { return apiTokens_1.resolveApiToken; } });
12
12
  Object.defineProperty(exports, "API_TOKEN_PREFIX", { enumerable: true, get: function () { return apiTokens_1.API_TOKEN_PREFIX; } });
13
+ var apiTokenPermissions_1 = require("./services/apiTokenPermissions");
14
+ Object.defineProperty(exports, "PROJECT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.PROJECT_PERMISSIONS; } });
15
+ Object.defineProperty(exports, "ACCOUNT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.ACCOUNT_PERMISSIONS; } });
@@ -6,9 +6,10 @@ export type ApiCaller = (call: ToolCall) => Promise<{
6
6
  status: number;
7
7
  body: unknown;
8
8
  }>;
9
+ /** The tools an API token may use: those whose every permission it has. */
10
+ export declare const toolsFor: (scope: Auth["apiToken"]) => import("./tools").ToolDefinition<any>[];
9
11
  /**
10
- * An MCP server for one request of `auth` (an API token's owner): its tools, the writing ones
11
- * only for a token with write access. Each tool call goes to the REST API through `api`, and is
12
- * recorded in the audit log.
12
+ * An MCP server for one request made with an API token (`auth`): only the tools its permissions
13
+ * allow. Each tool call goes to the REST API through `api`, and is recorded in the audit log.
13
14
  */
14
15
  export declare function createMcpServer(auth: Auth, api: ApiCaller, config: StreamByConfig, version: string): McpServer;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MCP_SERVER_NAME = void 0;
3
+ exports.toolsFor = exports.MCP_SERVER_NAME = void 0;
4
4
  exports.createMcpServer = createMcpServer;
5
5
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
6
  const tools_1 = require("./tools");
@@ -10,25 +10,28 @@ const errorText = (body, status) => {
10
10
  const message = body?.message ?? body?.error;
11
11
  return typeof message === 'string' ? message : `StreamBy answered ${status}`;
12
12
  };
13
+ /** The tools an API token may use: those whose every permission it has. */
14
+ const toolsFor = (scope) => tools_1.TOOLS.filter(tool => tool.permissions.every(p => !!scope?.permissions.includes(p)));
15
+ exports.toolsFor = toolsFor;
13
16
  /**
14
- * An MCP server for one request of `auth` (an API token's owner): its tools, the writing ones
15
- * only for a token with write access. Each tool call goes to the REST API through `api`, and is
16
- * recorded in the audit log.
17
+ * An MCP server for one request made with an API token (`auth`): only the tools its permissions
18
+ * allow. Each tool call goes to the REST API through `api`, and is recorded in the audit log.
17
19
  */
18
20
  function createMcpServer(auth, api, config, version) {
19
21
  const server = new mcp_js_1.McpServer({ name: exports.MCP_SERVER_NAME, version });
20
- const canWrite = auth.apiToken?.access === 'write';
21
- for (const tool of tools_1.TOOLS) {
22
- if (tool.write && !canWrite)
23
- continue;
22
+ // A project's token acts on its project: what it does there is the project's.
23
+ const tokenProject = auth.apiToken?.kind === 'project' && Array.isArray(auth.apiToken.projectIds)
24
+ ? auth.apiToken.projectIds[0] ?? null
25
+ : null;
26
+ for (const tool of (0, exports.toolsFor)(auth.apiToken)) {
24
27
  server.registerTool(tool.name, {
25
28
  title: tool.title,
26
29
  description: tool.description,
27
30
  inputSchema: tool.input,
28
- annotations: { readOnlyHint: !tool.write, destructiveHint: false, openWorldHint: false },
31
+ annotations: { readOnlyHint: !tool.write, destructiveHint: !!tool.destructive, openWorldHint: false },
29
32
  }, async (args) => {
30
33
  const started = Date.now();
31
- const projectId = tool.projectOf?.(args) ?? null;
34
+ const projectId = tool.projectOf?.(args) ?? tokenProject;
32
35
  let status = null;
33
36
  try {
34
37
  const answer = await api(tool.call(args));
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { ApiTokenPermission } from '../services/apiTokenPermissions';
2
3
  export interface ToolCall {
3
- method: 'GET' | 'POST' | 'PATCH' | 'PUT';
4
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
4
5
  path: string;
5
6
  body?: unknown;
6
7
  }
@@ -8,7 +9,9 @@ export interface ToolDefinition<S extends z.ZodRawShape = z.ZodRawShape> {
8
9
  name: string;
9
10
  title: string;
10
11
  description: string;
11
- write: boolean;
12
+ permissions: ApiTokenPermission[];
13
+ write?: boolean;
14
+ destructive?: boolean;
12
15
  input: S;
13
16
  projectOf?: (args: z.infer<z.ZodObject<S>>) => string | undefined;
14
17
  call: (args: z.infer<z.ZodObject<S>>) => ToolCall;
package/dist/mcp/tools.js CHANGED
@@ -38,21 +38,21 @@ const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([,
38
38
  exports.TOOLS = [
39
39
  // ── Reading ──
40
40
  tool({
41
- name: 'list_projects', title: 'List projects', write: false,
41
+ name: 'list_projects', title: 'List projects', permissions: [],
42
42
  description: 'The StreamBy projects this token reaches.',
43
43
  input: {},
44
44
  call: () => ({ method: 'GET', path: '/projects' }),
45
45
  pick: (data) => data.projects,
46
46
  }),
47
47
  tool({
48
- name: 'get_project', title: 'Get a project', write: false,
48
+ name: 'get_project', title: 'Get a project', permissions: ['project:read'],
49
49
  description: "A project's details: its name, description, members, exports and connections.",
50
50
  input: { projectId },
51
51
  projectOf: byProject,
52
52
  call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}` }),
53
53
  }),
54
54
  tool({
55
- name: 'list_assets', title: 'List assets', write: false,
55
+ name: 'list_assets', title: 'List assets', permissions: ['assets:read'],
56
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
57
  input: {
58
58
  projectId,
@@ -77,42 +77,42 @@ exports.TOOLS = [
77
77
  },
78
78
  }),
79
79
  tool({
80
- name: 'get_asset_metadata', title: "Get an asset's metadata", write: false,
80
+ name: 'get_asset_metadata', title: "Get an asset's metadata", permissions: ['assets:read'],
81
81
  description: "An asset's metadata (technical data and custom tags).",
82
82
  input: { projectId, fileId: id('asset') },
83
83
  projectOf: byProject,
84
84
  call: ({ projectId, fileId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata` }),
85
85
  }),
86
86
  tool({
87
- name: 'get_asset_rights', title: "Get an asset's rights", write: false,
87
+ name: 'get_asset_rights', title: "Get an asset's rights", permissions: ['rights:read'],
88
88
  description: "An asset's rights: holder, license, territory, expiry and restrictions.",
89
89
  input: { projectId, assetId: id('asset') },
90
90
  projectOf: byProject,
91
91
  call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights` }),
92
92
  }),
93
93
  tool({
94
- name: 'get_workflow', title: "Get a project's workflow", write: false,
94
+ name: 'get_workflow', title: "Get a project's workflow", permissions: ['workflow:read'],
95
95
  description: "A project's workflow (its node graph).",
96
96
  input: { projectId },
97
97
  projectOf: byProject,
98
98
  call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/workflow` }),
99
99
  }),
100
100
  tool({
101
- name: 'list_pipelines', title: 'List pipelines', write: false,
101
+ name: 'list_pipelines', title: 'List pipelines', permissions: ['workflow:read'],
102
102
  description: "A project's pipelines.",
103
103
  input: { projectId },
104
104
  projectOf: byProject,
105
105
  call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines` }),
106
106
  }),
107
107
  tool({
108
- name: 'get_pipeline', title: 'Get a pipeline', write: false,
108
+ name: 'get_pipeline', title: 'Get a pipeline', permissions: ['workflow:read'],
109
109
  description: 'A pipeline, with its node schema.',
110
110
  input: { projectId, pipelineId: id('pipeline') },
111
111
  projectOf: byProject,
112
112
  call: ({ projectId, pipelineId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}` }),
113
113
  }),
114
114
  tool({
115
- name: 'list_exports', title: 'List exports', write: false,
115
+ name: 'list_exports', title: 'List exports', permissions: ['project:read', 'exports:read'],
116
116
  description: "A project's exports (its published endpoints).",
117
117
  input: { projectId },
118
118
  projectOf: byProject,
@@ -120,34 +120,34 @@ exports.TOOLS = [
120
120
  pick: (data) => data?.project?.exports ?? data?.exports ?? [],
121
121
  }),
122
122
  tool({
123
- name: 'get_export', title: 'Get an export', write: false,
123
+ name: 'get_export', title: 'Get an export', permissions: ['exports:read'],
124
124
  description: 'An export, with its node schema.',
125
125
  input: { projectId, exportId: id('export') },
126
126
  projectOf: byProject,
127
127
  call: ({ projectId, exportId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/exports/${enc(exportId)}` }),
128
128
  }),
129
129
  tool({
130
- name: 'get_job', title: 'Get a job', write: false,
130
+ name: 'get_job', title: 'Get a job', permissions: ['jobs:read'],
131
131
  description: "A job's status and progress (transcoding, rendering, ingesting...).",
132
132
  input: { jobId: id('job') },
133
133
  call: ({ jobId }) => ({ method: 'GET', path: `/jobs/${enc(jobId)}` }),
134
134
  }),
135
135
  tool({
136
- name: 'list_sequences', title: 'List production sequences', write: false,
136
+ name: 'list_sequences', title: 'List production sequences', permissions: ['production:read'],
137
137
  description: "A project's production sequences.",
138
138
  input: { projectId },
139
139
  projectOf: byProject,
140
140
  call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences` }),
141
141
  }),
142
142
  tool({
143
- name: 'list_shots', title: 'List shots', write: false,
143
+ name: 'list_shots', title: 'List shots', permissions: ['production:read'],
144
144
  description: "A production sequence's shots.",
145
145
  input: { projectId, sequenceId: id('sequence') },
146
146
  projectOf: byProject,
147
147
  call: ({ projectId, sequenceId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots` }),
148
148
  }),
149
149
  tool({
150
- name: 'list_tasks', title: 'List shot tasks', write: false,
150
+ name: 'list_tasks', title: 'List shot tasks', permissions: ['production:read'],
151
151
  description: "A shot's tasks.",
152
152
  input: { projectId, shotId: id('shot') },
153
153
  projectOf: byProject,
@@ -155,73 +155,132 @@ exports.TOOLS = [
155
155
  }),
156
156
  // ── Writing ──
157
157
  tool({
158
- name: 'update_asset_metadata', title: "Update an asset's custom tags", write: true,
158
+ name: 'update_asset_metadata', title: "Update an asset's custom tags", permissions: ['assets:write'], write: true,
159
159
  description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had.",
160
160
  input: { projectId, fileId: id('asset'), customTags: zod_1.z.record(zod_1.z.string(), zod_1.z.string()) },
161
161
  projectOf: byProject,
162
162
  call: ({ projectId, fileId, customTags }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata`, body: { customTags } }),
163
163
  }),
164
164
  tool({
165
- name: 'set_asset_rights', title: "Set an asset's rights", write: true,
165
+ name: 'set_asset_rights', title: "Set an asset's rights", permissions: ['rights:write'], write: true,
166
166
  description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes.",
167
167
  input: { projectId, assetId: id('asset'), ...rightsFields },
168
168
  projectOf: byProject,
169
169
  call: ({ projectId, assetId, ...rights }) => ({ method: 'PUT', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights`, body: defined(rights) }),
170
170
  }),
171
171
  tool({
172
- name: 'create_pipeline', title: 'Create a pipeline', write: true,
172
+ name: 'create_pipeline', title: 'Create a pipeline', permissions: ['workflow:write'], write: true,
173
173
  description: 'Creates a pipeline in a project.',
174
174
  input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
175
175
  projectOf: byProject,
176
176
  call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/pipelines`, body: defined(fields) }),
177
177
  }),
178
178
  tool({
179
- name: 'update_pipeline', title: 'Update a pipeline', write: true,
179
+ name: 'update_pipeline', title: 'Update a pipeline', permissions: ['workflow:write'], write: true,
180
180
  description: "Changes a pipeline's name, description or order.",
181
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
182
  projectOf: byProject,
183
183
  call: ({ projectId, pipelineId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}`, body: defined(fields) }),
184
184
  }),
185
185
  tool({
186
- name: 'create_sequence', title: 'Create a production sequence', write: true,
186
+ name: 'create_sequence', title: 'Create a production sequence', permissions: ['production:write'], write: true,
187
187
  description: 'Creates a production sequence in a project.',
188
188
  input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
189
189
  projectOf: byProject,
190
190
  call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences`, body: defined(fields) }),
191
191
  }),
192
192
  tool({
193
- name: 'update_sequence', title: 'Update a production sequence', write: true,
193
+ name: 'update_sequence', title: 'Update a production sequence', permissions: ['production:write'], write: true,
194
194
  description: "Changes a sequence's name, description or order.",
195
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
196
  projectOf: byProject,
197
197
  call: ({ projectId, sequenceId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}`, body: defined(fields) }),
198
198
  }),
199
199
  tool({
200
- name: 'create_shot', title: 'Create a shot', write: true,
200
+ name: 'create_shot', title: 'Create a shot', permissions: ['production:write'], write: true,
201
201
  description: 'Creates a shot in a production sequence.',
202
202
  input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1), ...shotFields },
203
203
  projectOf: byProject,
204
204
  call: ({ projectId, sequenceId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots`, body: defined(fields) }),
205
205
  }),
206
206
  tool({
207
- name: 'update_shot', title: 'Update a shot', write: true,
207
+ name: 'update_shot', title: 'Update a shot', permissions: ['production:write'], write: true,
208
208
  description: 'Changes a shot (its name, status, assignee, due date...).',
209
209
  input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1).optional(), ...shotFields },
210
210
  projectOf: byProject,
211
211
  call: ({ projectId, shotId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}`, body: defined(fields) }),
212
212
  }),
213
213
  tool({
214
- name: 'create_task', title: 'Create a shot task', write: true,
214
+ name: 'create_task', title: 'Create a shot task', permissions: ['production:write'], write: true,
215
215
  description: 'Creates a task in a shot.',
216
216
  input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1), ...taskFields },
217
217
  projectOf: byProject,
218
218
  call: ({ projectId, shotId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks`, body: defined(fields) }),
219
219
  }),
220
220
  tool({
221
- name: 'update_task', title: 'Update a shot task', write: true,
221
+ name: 'update_task', title: 'Update a shot task', permissions: ['production:write'], write: true,
222
222
  description: 'Changes a task (its name, status, priority, assignee, due date, notes).',
223
223
  input: { projectId, shotId: id('shot'), taskId: id('task'), name: zod_1.z.string().min(1).optional(), ...taskFields },
224
224
  projectOf: byProject,
225
225
  call: ({ projectId, shotId, taskId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}`, body: defined(fields) }),
226
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
+ }),
227
286
  ];