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
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TASK_PRIORITIES = exports.PRODUCTION_STATUSES = void 0;
4
+ exports.checkProductionFields = checkProductionFields;
5
+ const auth_1 = require("../utils/auth");
6
+ // What a production sequence, shot or task may be given, checked before it's saved: a name
7
+ // that isn't blank, a known status and priority, assignees who are members of the project, a
8
+ // due date that is a date.
9
+ exports.PRODUCTION_STATUSES = ['todo', 'in_progress', 'review', 'done'];
10
+ exports.TASK_PRIORITIES = ['low', 'medium', 'high'];
11
+ const MAX_NAME = 200;
12
+ const MAX_TEXT = 5000;
13
+ /**
14
+ * The fields of `body` that `allowed` lists, checked and ready to save (`assignees` and
15
+ * `assignee` are saved as `assignedTo`). With `creating`, a name is required.
16
+ */
17
+ function checkProductionFields(body, allowed, project, { creating = false } = {}) {
18
+ const values = {};
19
+ const given = (field) => body?.[field] !== undefined;
20
+ const has = (field) => allowed.includes(field);
21
+ if (creating && !given('name'))
22
+ return { error: 'name is required' };
23
+ if (has('name') && given('name')) {
24
+ if (typeof body.name !== 'string' || !body.name.trim())
25
+ return { error: 'name must not be blank' };
26
+ if (body.name.trim().length > MAX_NAME)
27
+ return { error: `name must be at most ${MAX_NAME} characters` };
28
+ values.name = body.name.trim();
29
+ }
30
+ for (const field of ['description', 'notes']) {
31
+ if (!has(field) || !given(field))
32
+ continue;
33
+ const value = body[field];
34
+ if (value !== null && (typeof value !== 'string' || value.length > MAX_TEXT)) {
35
+ return { error: `${field} must be text of at most ${MAX_TEXT} characters, or null` };
36
+ }
37
+ values[field] = value === null || !value.trim() ? null : value;
38
+ }
39
+ if (has('status') && given('status')) {
40
+ if (!exports.PRODUCTION_STATUSES.includes(body.status))
41
+ return { error: `status must be one of ${exports.PRODUCTION_STATUSES.join(', ')}` };
42
+ values.status = body.status;
43
+ }
44
+ if (has('priority') && given('priority')) {
45
+ if (!exports.TASK_PRIORITIES.includes(body.priority))
46
+ return { error: `priority must be one of ${exports.TASK_PRIORITIES.join(', ')}` };
47
+ values.priority = body.priority;
48
+ }
49
+ const isMember = (id) => typeof id === 'string' && !!(0, auth_1.activeMemberOf)(project, id);
50
+ if (has('assignees') && given('assignedTo')) {
51
+ const ids = body.assignedTo;
52
+ if (!Array.isArray(ids) || !ids.every(isMember))
53
+ return { error: 'assignedTo must be a list of members of the project' };
54
+ values.assignedTo = [...new Set(ids)];
55
+ }
56
+ if (has('assignee') && given('assignedTo')) {
57
+ if (body.assignedTo !== null && !isMember(body.assignedTo))
58
+ return { error: 'assignedTo must be a member of the project, or null' };
59
+ values.assignedTo = body.assignedTo;
60
+ }
61
+ if (has('dueDate') && given('dueDate')) {
62
+ const date = body.dueDate === null || body.dueDate === '' ? null : new Date(body.dueDate);
63
+ if (date && Number.isNaN(date.getTime()))
64
+ return { error: 'dueDate must be a date, or null' };
65
+ values.dueDate = date;
66
+ }
67
+ if (has('order') && given('order')) {
68
+ if (typeof body.order !== 'number' || !Number.isFinite(body.order))
69
+ return { error: 'order must be a number' };
70
+ values.order = body.order;
71
+ }
72
+ for (const field of ['assetId', 'exportId']) {
73
+ if (!has(field) || !given(field))
74
+ continue;
75
+ if (body[field] !== null && typeof body[field] !== 'string')
76
+ return { error: `${field} must be an id, or null` };
77
+ values[field] = body[field] || null;
78
+ }
79
+ return { values };
80
+ }
@@ -0,0 +1,6 @@
1
+ import { Auth, StreamByConfig } from '../types';
2
+ export declare const DEFAULT_TOKEN_SIGNED_URL_TTL: {
3
+ upload: number;
4
+ download: number;
5
+ };
6
+ export declare const signedUrlTtl: (auth: Auth | undefined, config: StreamByConfig, kind: "upload" | "download") => number | undefined;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signedUrlTtl = exports.DEFAULT_TOKEN_SIGNED_URL_TTL = void 0;
4
+ // How long a signed storage URL works. A request made with an API token (an agent, the MCP
5
+ // server) gets short-lived ones; a browser session, the adapter's own (an hour to upload, a
6
+ // day to read).
7
+ exports.DEFAULT_TOKEN_SIGNED_URL_TTL = { upload: 15 * 60, download: 5 * 60 };
8
+ const signedUrlTtl = (auth, config, kind) => auth?.apiToken ? config.mcp?.signedUrlTtl?.[kind] ?? exports.DEFAULT_TOKEN_SIGNED_URL_TTL[kind] : undefined;
9
+ exports.signedUrlTtl = signedUrlTtl;
@@ -1,3 +1,4 @@
1
+ import type { ApiTokenPermission, ProjectPermission, AccountPermission } from '../services/apiTokenPermissions';
1
2
  import { Request } from 'express';
2
3
  import type { WebSocketServer } from 'ws';
3
4
  export interface S3Config {
@@ -41,10 +42,11 @@ export interface StorageAdapter {
41
42
  deleteProjectDirectory: (projectId: string) => Promise<any>;
42
43
  getPresignedUrl?: (contentType: string, projectId: string) => Promise<any>;
43
44
  getPresignedProjectImageUrl?: (projectId: string) => Promise<any>;
44
- getPresignedUploadUrl?: (key: string, contentType: string) => Promise<string>;
45
+ getPresignedUploadUrl?: (key: string, contentType: string, expiresIn?: number) => Promise<string>;
45
46
  listFilesByCategory?: (projectId: string, category: string) => Promise<StorageFileInfo[]>;
46
47
  deleteFile?: (key: string) => Promise<void>;
47
- getPresignedGetUrl?: (key: string) => Promise<string>;
48
+ copyFile?: (from: string, to: string) => Promise<void>;
49
+ getPresignedGetUrl?: (key: string, expiresIn?: number, downloadName?: string) => Promise<string>;
48
50
  }
49
51
  export interface FieldDefinition {
50
52
  name: string;
@@ -139,6 +141,10 @@ export interface StreamByConfig {
139
141
  windowMs: number;
140
142
  };
141
143
  apiBaseUrl?: string;
144
+ signedUrlTtl?: {
145
+ upload?: number;
146
+ download?: number;
147
+ };
142
148
  };
143
149
  websocket?: {
144
150
  server: WebSocketServer;
@@ -167,10 +173,12 @@ export interface Auth {
167
173
  role: 'viewer' | 'editor' | 'admin';
168
174
  apiToken?: ApiTokenScope;
169
175
  }
170
- export type ApiTokenAccess = 'read' | 'write';
176
+ export type { ApiTokenPermission, ProjectPermission, AccountPermission };
177
+ export type ApiTokenKind = 'user' | 'project';
171
178
  export interface ApiTokenScope {
172
179
  tokenId: string;
173
- access: ApiTokenAccess;
180
+ kind: ApiTokenKind;
181
+ permissions: ApiTokenPermission[];
174
182
  projectIds: string[] | '*';
175
183
  }
176
184
  export type AuthProvider = (req: Request) => Promise<Auth>;
@@ -1,10 +1,21 @@
1
1
  import { ProjectInfo } from '../types';
2
- type ProjectMembers = Pick<ProjectInfo, 'members'> | null | undefined;
2
+ type ProjectMembers = (Pick<ProjectInfo, 'members'> & {
3
+ _id?: unknown;
4
+ id?: unknown;
5
+ }) | null | undefined;
6
+ export declare const projectTokenPrincipal: (tokenId: string, projectId: string) => string;
7
+ export declare const isProjectTokenPrincipal: (userId: unknown) => boolean;
3
8
  export declare function activeMemberOf(project: ProjectMembers, userId: string | undefined): {
4
9
  role: "viewer" | "editor" | "admin";
5
10
  userId: string;
6
11
  archived?: boolean;
7
12
  status?: "active" | "pending";
13
+ } | {
14
+ userId: string;
15
+ username: string;
16
+ role: "admin";
17
+ status: "active";
18
+ archived: boolean;
8
19
  } | undefined;
9
20
  export declare const projectRoleOf: (project: ProjectMembers, userId: string | undefined) => "viewer" | "editor" | "admin" | undefined;
10
21
  /** An active member, whatever their role: can see the project. */
@@ -1,11 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isProjectAdmin = exports.canEditProject = exports.isProjectMember = exports.projectRoleOf = void 0;
3
+ exports.isProjectAdmin = exports.canEditProject = exports.isProjectMember = exports.projectRoleOf = exports.isProjectTokenPrincipal = exports.projectTokenPrincipal = void 0;
4
4
  exports.activeMemberOf = activeMemberOf;
5
+ // A project's API token acts on its project as itself, not as a user: its request's userId is
6
+ // this principal, naming the token and its project. In that project it's an admin -- what it
7
+ // may do is only its permissions (see enforceApiTokenScope) -- and in any other, nobody.
8
+ const PROJECT_TOKEN_PRINCIPAL = 'project-token:';
9
+ const projectTokenPrincipal = (tokenId, projectId) => `${PROJECT_TOKEN_PRINCIPAL}${tokenId}:${projectId}`;
10
+ exports.projectTokenPrincipal = projectTokenPrincipal;
11
+ const isProjectTokenPrincipal = (userId) => typeof userId === 'string' && userId.startsWith(PROJECT_TOKEN_PRINCIPAL);
12
+ exports.isProjectTokenPrincipal = isProjectTokenPrincipal;
13
+ const projectOfPrincipal = (userId) => {
14
+ if (!(0, exports.isProjectTokenPrincipal)(userId))
15
+ return undefined;
16
+ const rest = userId.slice(PROJECT_TOKEN_PRINCIPAL.length);
17
+ return rest.slice(rest.indexOf(':') + 1) || undefined;
18
+ };
5
19
  // A project's member who's in it now: invited and accepted (status 'active'). (A member's
6
20
  // `archived` is only that member having archived the project in their own list.)
7
21
  function activeMemberOf(project, userId) {
8
- if (!project?.members || !userId)
22
+ if (!project || !userId)
23
+ return undefined;
24
+ const tokenProject = projectOfPrincipal(userId);
25
+ if (tokenProject !== undefined) {
26
+ const id = String(project._id ?? project.id ?? '');
27
+ return id && id === tokenProject ? { userId, username: 'API token', role: 'admin', status: 'active', archived: false } : undefined;
28
+ }
29
+ if (!project.members)
9
30
  return undefined;
10
31
  return project.members.find(member => member.userId?.toString() === userId && member.status === 'active');
11
32
  }
@@ -0,0 +1,2 @@
1
+ /** The NoSQL database the project's files and production live in, or null when there's none. */
2
+ export declare function nosqlDb(): import("mongodb").Db | null;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nosqlDb = nosqlDb;
4
+ const connectionManager_1 = require("../adapters/database/connectionManager");
5
+ /** The NoSQL database the project's files and production live in, or null when there's none. */
6
+ function nosqlDb() {
7
+ const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
8
+ if (!ids.length)
9
+ return null;
10
+ return (0, connectionManager_1.getConnection)(ids[0]).client.db();
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamby-core",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "StreamBy middleware framework for media storage management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",