streamby-core 0.42.0 → 0.43.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 (38) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +92 -4
  3. package/dist/adapters/database/connectionManager.js +1 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +5 -1
  6. package/dist/mcp/tools.js +25 -11
  7. package/dist/mcp/workflowNodes.js +9 -3
  8. package/dist/middleware/createRouter.js +2 -0
  9. package/dist/middleware/routes/connection.js +5 -2
  10. package/dist/middleware/routes/dbConnection.js +29 -5
  11. package/dist/middleware/routes/document.js +11 -6
  12. package/dist/middleware/routes/export.d.ts +28 -0
  13. package/dist/middleware/routes/export.js +8 -4
  14. package/dist/middleware/routes/pipeline.js +5 -2
  15. package/dist/middleware/routes/project.js +14 -12
  16. package/dist/middleware/routes/projectResources.d.ts +8 -0
  17. package/dist/middleware/routes/projectResources.js +51 -0
  18. package/dist/middleware/routes/rights.js +72 -52
  19. package/dist/middleware/routes/storage.js +35 -2
  20. package/dist/middleware/routes/storageConnection.js +38 -15
  21. package/dist/middleware/routes/userIntegration.js +42 -1
  22. package/dist/middleware/routes/workflow.js +120 -37
  23. package/dist/models/Model.js +12 -1
  24. package/dist/services/apiTokenPermissions.js +2 -0
  25. package/dist/services/creditsSheets.d.ts +39 -0
  26. package/dist/services/creditsSheets.js +129 -0
  27. package/dist/services/dependents.d.ts +11 -0
  28. package/dist/services/dependents.js +81 -0
  29. package/dist/services/production.d.ts +1 -1
  30. package/dist/services/production.js +2 -2
  31. package/dist/services/projectIntegration.d.ts +14 -0
  32. package/dist/services/projectIntegration.js +32 -0
  33. package/dist/services/rights.d.ts +44 -0
  34. package/dist/services/rights.js +123 -0
  35. package/dist/services/workflowResources.d.ts +90 -0
  36. package/dist/services/workflowResources.js +288 -0
  37. package/dist/types/index.d.ts +6 -1
  38. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.43.0
4
+
5
+ ### Added
6
+
7
+ - **License metadata of assets**, for crediting the ones taken from others: `sourceUrl`,
8
+ `creatorName`, `creatorUrl`, `licenseType` and `comments`, all optional, in `asset_rights`.
9
+ The license is one of a closed list (`LICENSE_TYPES`: CC BY, CC BY-SA, CC BY-ND, CC BY-NC,
10
+ CC BY-NC-SA, CC BY-NC-ND, CC0, Free Standard, Standard, Editorial), checked by the server; the
11
+ links are http(s) URLs. It can be given with an upload (query params of `GET .../upload-url`;
12
+ an API token needs `rights:write` too). An `assetRightsEvent` to the project's active members
13
+ on each change, and when an asset is deleted.
14
+ - **Credits sheets**: project documents of a new kind, `credits`, that a workflow's Credits sheet
15
+ node makes (`CREDITS_SHEET_NODE`). One keeps only its sources (assets and Storage folders picked,
16
+ and the Ingest nodes connected to its node); its rows are made when it's read, from each asset's
17
+ metadata, with its status: complete when it has an asset and each has its creator, source and
18
+ license. Routes `GET /projects/:projectId/credits-sheets` and `.../credits-sheets/:docId`
19
+ (`rights:read`). Types `CreditsSheet`, `SheetRow`, `SheetSources` and `SheetStatus`.
20
+ - MCP: `get_credits_sheet`; `request_asset_upload` takes the license metadata.
21
+ - **The workflow defines the project.** Each resource is a node of it, bound by the resource's
22
+ id in its data. A resource added or removed from its section (`POST`/`DELETE` of a connection,
23
+ an API, an export, a pipeline, a project integration) adds or removes its node; saving the
24
+ workflow connects what its nodes were set to (`"__connect__:<integrationId>"`), makes the APIs
25
+ drafted on them (`data.connectionDraft`) and removes the resources whose nodes are gone,
26
+ answering `{ data, project, errors }`. A save needs `baseUpdatedAt` (a 409 over a newer version)
27
+ and a `nodeSchema` with `nodes` and `edges` lists (a 400 otherwise); an app connected with OAuth
28
+ can't remove resources through it (a 403 listing them). Pipelines and exports drafted on nodes
29
+ are made on save too. A node with a configuration of its own (a data source's table, an
30
+ Ingest's file) consumes its resource and is never merged into its node.
31
+ A workflow made before this is brought to it once, when it's read (`resourceNodesAt`).
32
+ - **What depends on a resource**, asked before removing it: `GET /projects/:id/dependents/:kind/:resourceId`
33
+ (connections, exports, pipelines): the workflow's, exports' and pipelines' nodes that use it,
34
+ and the files, folders, deliverables and shots kept for it. `GET /user/integrations/:id/dependents`:
35
+ the project connections that use an account's integration.
36
+ - **A change to a project's resources answers its list as it is then**, for the client to show
37
+ without asking again: connecting or disconnecting a database (`dbConnections`), a storage
38
+ (`storageConnections`, with built-ins' `available`) or an API (`connections`), deleting an
39
+ export (`exports`) or a pipeline (`pipelines`); `POST`/`DELETE /projects/:id/integrations`
40
+ answer the `project`; deleting an account's integration, each project it was disconnected
41
+ from (`affected`).
42
+ - **A built-in connected again keeps the id it had** (`disconnectedBuiltins` on the project; a
43
+ new `disconnectedBuiltins` column in a PostgreSQL projects table, added at startup), so its
44
+ files and records are its own again.
45
+
46
+ ### Changed
47
+
48
+ - **`asset_rights` holds the license metadata only.** `rightsHolder`, `licenseUrl`, `territory`,
49
+ `expiresAt`, `usageRestrictions` and `notes` are gone: not read, and dropped from a record when
50
+ it's next saved; a `licenseType` off the list reads as none. `set_asset_rights` and
51
+ `get_asset_rights` take and give the new fields.
52
+
53
+ ### Fixed
54
+
55
+ - Deleting an asset or a project deletes its license metadata (it was left behind).
56
+ - A built-in database or storage can be disconnected from a project (it was refused with a 403,
57
+ though the backfill already left a disconnected one alone): what the project keeps in it stays.
58
+ - Deleting an account's integration disconnects the project connections that use it (they were
59
+ left pointing at nothing).
60
+ - `POST .../connections/db` and `.../connections/storage` with an `integrationId` connect a
61
+ built-in too (they refused it as an unknown integration, and checked the pool's `sql`/`nosql`
62
+ as the type): with an integration, its name and type are the integration's.
63
+
3
64
  ## 0.42.0
4
65
 
5
66
  ### Added
package/README.md CHANGED
@@ -132,7 +132,7 @@ hash is stored, with a short prefix to tell it apart.
132
132
  | Production | `production:read`, `production:write`, `production:delete` | Sequences, shots and tasks. |
133
133
  | Workflow | `workflow:read`, `workflow:write` | The workflow and its pipelines. |
134
134
  | Exports | `exports:read`, `exports:write` | The project's exports. |
135
- | Rights | `rights:read`, `rights:write` | Assets' rights. |
135
+ | Rights | `rights:read`, `rights:write` | Assets' license metadata, and reading credits sheets. |
136
136
  | Jobs | `jobs:read`, `jobs:run` | Following jobs; starting them (transcoding, rendering, AI, publishing a deliverable). |
137
137
  | Documents | `docs:read`, `docs:write`, `docs:publish`, `docs:delete` | The project's documents: reading them and their history; creating, editing and moving them; publishing them (the version its exports deliver, and whether they deliver it); deleting them. |
138
138
  | Account | `projects:create` | Creating projects. A user's token only; the token reaches what it creates. |
@@ -285,7 +285,7 @@ A token sees only the tools whose permissions it has (an app: those its scopes g
285
285
  | `list_assets` (by category, optionally in one storage connection, and filtered by name), `get_asset_metadata` | `assets:read` |
286
286
  | `update_asset_metadata` | `assets:write` |
287
287
  | `delete_asset` | `assets:delete` |
288
- | `get_asset_rights` / `set_asset_rights` | `rights:read` / `rights:write` |
288
+ | `get_asset_rights`, `get_credits_sheet` / `set_asset_rights` | `rights:read` / `rights:write` |
289
289
  | `get_workflow`, `list_pipelines`, `get_pipeline` | `workflow:read` |
290
290
  | `create_pipeline`, `update_pipeline` | `workflow:write` |
291
291
  | `list_exports` | `project:read` and `exports:read` |
@@ -298,7 +298,7 @@ A token sees only the tools whose permissions it has (an app: those its scopes g
298
298
  | `create_sequence`, `create_shot`, `create_task` | `production:write` and `workflow:write` |
299
299
  | `delete_sequence`, `delete_shot`, `delete_task` | `production:delete` |
300
300
  | `list_asset_versions`, `get_asset_download_url`, `list_deliverables` | `assets:read` |
301
- | `request_asset_upload`, `mark_deliverable`, `remove_deliverable` | `assets:write` |
301
+ | `request_asset_upload` (with license metadata, also `rights:write`), `mark_deliverable`, `remove_deliverable` | `assets:write` |
302
302
  | `list_documents`, `get_document`, `list_document_versions` | `docs:read` |
303
303
  | `create_document`, `update_document`, `update_document_section` | `docs:write` |
304
304
 
@@ -476,7 +476,7 @@ or collections), each one in a single project, in folders.
476
476
  | Route | Permission |
477
477
  |---|---|
478
478
  | `GET /projects/:projectId/documents`: the tree, without contents | `docs:read` |
479
- | `POST /projects/:projectId/documents` `{ title, content?, kind?: 'document' \| 'folder', parentId?, position? }` | `docs:write` |
479
+ | `POST /projects/:projectId/documents` `{ title, content?, kind?: 'document' \| 'folder' \| 'credits', parentId?, position? }` | `docs:write` |
480
480
  | `GET /projects/:projectId/documents/:docId` | `docs:read` |
481
481
  | `PATCH /projects/:projectId/documents/:docId` `{ baseVersion, title?, content? }` (a new version) and/or `{ parentId?, position? }` | `docs:write` |
482
482
  | `PATCH /projects/:projectId/documents/:docId` `{ exportable }` | `docs:write` and `docs:publish` |
@@ -493,6 +493,44 @@ In MongoDB, a save writes the document and its version in a transaction when the
493
493
 
494
494
  ---
495
495
 
496
+ ## ©️ License metadata and credits sheets
497
+
498
+ Each asset can carry its **license metadata**, for all its versions: where it was taken from
499
+ (`sourceUrl`), who made it (`creatorName`, `creatorUrl`), its license (`licenseType`) and
500
+ `comments`. All are optional; the links are http(s) URLs, and the license is one of
501
+ `CC BY`, `CC BY-SA`, `CC BY-ND`, `CC BY-NC`, `CC BY-NC-SA`, `CC BY-NC-ND`, `CC0`, `Free Standard`,
502
+ `Standard` and `Editorial` (`LICENSE_TYPES`, exported). It's kept in the `asset_rights`
503
+ collection, the only place it lives, and goes with its asset or its project when they're deleted.
504
+ It can be given with the upload: `licenseType`, `sourceUrl`, `creatorName`, `creatorUrl` and
505
+ `comments` as query params of `GET .../upload-url` (an API token needs `rights:write` too).
506
+
507
+ A **credits sheet** is a project document of kind `credits`, made by a Credits sheet node of the
508
+ workflow (its `data.documentId`). It keeps only what feeds it, as its content:
509
+ `{ "assetIds": [...], "folderIds": [...] }` (assets and Storage folders picked, with everything
510
+ inside them); the Ingest nodes connected to its node feed it too, with the asset each imports
511
+ (`data.fileId`). Its rows are made each time it's read, from each asset's metadata, so two sheets
512
+ with the same asset show the same, and editing a row is setting its asset's metadata. Deleting
513
+ the node leaves the document; an asset deleted leaves the sheet.
514
+
515
+ A sheet is **complete** when it has an asset and each one has its creator, source and license;
516
+ **incomplete** otherwise, each row listing what it lacks (`missing`).
517
+
518
+ - **In real time**, an `assetRightsEvent` reaches the project's active members on each change:
519
+ `{ projectId, action: 'saved' | 'deleted', assetIds, rights? }` (`deleted`: the asset is gone).
520
+
521
+ | Route | Permission |
522
+ |---|---|
523
+ | `GET /projects/:projectId/assets/:assetId/rights` | `rights:read` |
524
+ | `PUT /projects/:projectId/assets/:assetId/rights` `{ sourceUrl?, creatorName?, creatorUrl?, licenseType?, comments? }` (the fields given; empty or `null` clears one) | `rights:write` |
525
+ | `GET /projects/:projectId/credits-sheets`: each with `{ id, title, status, rows, incomplete }` | `rights:read` |
526
+ | `GET /projects/:projectId/credits-sheets/:docId`: `{ document, sources, rows, status }` | `rights:read` |
527
+
528
+ Each row: `{ assetId, name, category, sourceUrl, creatorName, creatorUrl, licenseType, comments, missing, from }`,
529
+ `from` saying how it got there (`asset`, `folder`, `ingest`). Editors and admins set the metadata
530
+ (as they edit the asset); every member reads it.
531
+
532
+ ---
533
+
496
534
  ## ⚙️ Processing workers
497
535
 
498
536
  StreamBy doesn't process assets itself (optimizing a glTF, making LODs, transcoding...): it
@@ -598,6 +636,56 @@ A job its worker doesn't complete before its deadline is sent again, up to
598
636
 
599
637
  ---
600
638
 
639
+ ## 🧭 The workflow defines the project
640
+
641
+ Each of a project's resources (a database, an API, a storage, a service, an export, a pipeline)
642
+ is a node of its workflow, bound to it by the resource's id in its data (`connectionId`,
643
+ `storageConnectionId`, `integrationConnectionId`, `exportId`, `pipelineId`). One node per
644
+ resource; a data source reading one of a database's tables is a node of its own.
645
+
646
+ - **From a section** (or an agent): adding a resource adds its node to the saved workflow, and
647
+ removing it removes its nodes.
648
+ - **Saving the workflow** (`PATCH /projects/:id/workflow` `{ nodeSchema, baseUpdatedAt }`) makes
649
+ the project what it defines, then answers `{ data, project, errors }`: the workflow, the
650
+ project's lists as they are then, and what couldn't be done. A node set to an integration the
651
+ project doesn't have (`"__connect__:<integrationId>"`) connects it; an API, a pipeline or an
652
+ export drafted on a node (`data.connectionDraft`, `data.pipelineDraft`, `data.exportDraft`) is
653
+ made; a resource whose nodes are all gone is removed.
654
+ - `baseUpdatedAt` (the workflow's `updatedAt` as it was read) is required with a `nodeSchema`,
655
+ and `nodeSchema` must be `{ nodes: [...], edges: [...] }`: a 400 otherwise. Saved over a newer
656
+ version, it's a 409 with the workflow as it is now.
657
+ - An app connected with OAuth removes nothing through it, as it deletes nothing: a save that
658
+ would remove resources is a 403 listing them (`removing`).
659
+ - A node holding a configuration of its own beside its resource's id (a data source reading one
660
+ table, an Ingest importing one file) consumes the resource: never merged into its node.
661
+ - **Reading it** (`GET`) never rewrites it, except once, for a workflow made before this: the
662
+ nodes once drawn from the project's lists are bound by their ids, a resource without a node gets
663
+ one, and two nodes of the same resource are merged.
664
+
665
+ ---
666
+
667
+ ## 🔌 Removing a project's resources
668
+
669
+ Before a connection (database, API, storage, service), an export or a pipeline is removed, the
670
+ UI asks what depends on it: `GET /projects/:id/dependents/:kind/:resourceId` (`kind`: `db`,
671
+ `api`, `storage`, `service`, `export`, `pipeline`; `project:read`) answers
672
+ `{ dependents: [{ type, name, id?, count? }] }`: the nodes that use it in the workflow, the
673
+ exports and the pipelines (`workflow`, `export`, `pipeline`: they stop working without it), and
674
+ what's kept for it (`files` and `folders` of a storage, `deliverables` and `shots` of an
675
+ export). Removing it changes none of them.
676
+
677
+ **Built-ins** (the host's database and storage) are disconnected like any connection, and what
678
+ the project keeps in them stays. They're connected again as any integration is, by its id in
679
+ the pool (`GET /user/integrations`): `POST /projects/:id/integrations` `{ integrationId }`, or
680
+ `POST .../connections/db` and `.../connections/storage` with `integrationId` (its name and type
681
+ come from the integration). One connected again keeps the id it had (`disconnectedBuiltins`
682
+ on the project), so its files and records are its own again.
683
+
684
+ Deleting an account's integration disconnects the project connections that use it;
685
+ `GET /user/integrations/:id/dependents` lists them first.
686
+
687
+ ---
688
+
601
689
  ## 👥 Project roles
602
690
 
603
691
  What a user can do in a project depends on their role in that project, as an active member
@@ -101,6 +101,7 @@ const createTables = async (pool) => {
101
101
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
102
102
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
103
103
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
104
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "disconnectedBuiltins" JSONB DEFAULT '[]';
104
105
  -- Written when a project is made (routes/project) and by its connection routes.
105
106
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS public BOOLEAN DEFAULT TRUE;
106
107
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS category TEXT;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,10 @@ export { createNotification } from './services/notification';
4
4
  export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission, OAuthClientInfo, OAuthResourceConfig, ProjectDocument, DocumentVersion, DocumentAuthor, DocumentKind } from './types';
5
5
  export { resolveApiToken, API_TOKEN_PREFIX } from './services/apiTokens';
6
6
  export { PROJECT_PERMISSIONS, ACCOUNT_PERMISSIONS } from './services/apiTokenPermissions';
7
+ export { LICENSE_TYPES } from './services/rights';
8
+ export type { AssetRights, LicenseType } from './services/rights';
9
+ export { CREDITS_SHEET_NODE } from './services/creditsSheets';
10
+ export type { CreditsSheet, SheetRow, SheetSources, SheetStatus } from './services/creditsSheets';
7
11
  export type { ResolvedApiToken } from './services/apiTokens';
8
12
  export { createOAuthTokenVerifier, looksLikeOAuthAccessToken, protectedResourceMetadata, defaultResourceMetadataUrl, permissionsForOAuthScopes, OAUTH_SCOPES, OAUTH_SCOPE_PERMISSIONS, } from './services/oauthTokens';
9
13
  export type { VerifiedOAuthToken, OAuthTokenVerifierOptions, Jwks, Jwk, OAuthScope } from './services/oauthTokens';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OAUTH_SCOPE_PERMISSIONS = exports.OAUTH_SCOPES = exports.permissionsForOAuthScopes = exports.defaultResourceMetadataUrl = exports.protectedResourceMetadata = exports.looksLikeOAuthAccessToken = exports.createOAuthTokenVerifier = exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
3
+ exports.OAUTH_SCOPE_PERMISSIONS = exports.OAUTH_SCOPES = exports.permissionsForOAuthScopes = exports.defaultResourceMetadataUrl = exports.protectedResourceMetadata = exports.looksLikeOAuthAccessToken = exports.createOAuthTokenVerifier = exports.CREDITS_SHEET_NODE = exports.LICENSE_TYPES = 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");
@@ -13,6 +13,10 @@ Object.defineProperty(exports, "API_TOKEN_PREFIX", { enumerable: true, get: func
13
13
  var apiTokenPermissions_1 = require("./services/apiTokenPermissions");
14
14
  Object.defineProperty(exports, "PROJECT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.PROJECT_PERMISSIONS; } });
15
15
  Object.defineProperty(exports, "ACCOUNT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.ACCOUNT_PERMISSIONS; } });
16
+ var rights_1 = require("./services/rights");
17
+ Object.defineProperty(exports, "LICENSE_TYPES", { enumerable: true, get: function () { return rights_1.LICENSE_TYPES; } });
18
+ var creditsSheets_1 = require("./services/creditsSheets");
19
+ Object.defineProperty(exports, "CREDITS_SHEET_NODE", { enumerable: true, get: function () { return creditsSheets_1.CREDITS_SHEET_NODE; } });
16
20
  var oauthTokens_1 = require("./services/oauthTokens");
17
21
  Object.defineProperty(exports, "createOAuthTokenVerifier", { enumerable: true, get: function () { return oauthTokens_1.createOAuthTokenVerifier; } });
18
22
  Object.defineProperty(exports, "looksLikeOAuthAccessToken", { enumerable: true, get: function () { return oauthTokens_1.looksLikeOAuthAccessToken; } });
package/dist/mcp/tools.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TOOLS = void 0;
4
4
  const zod_1 = require("zod");
5
+ const rights_1 = require("../services/rights");
5
6
  const tool = (definition) => definition;
6
7
  const id = (what) => zod_1.z.string().min(1).describe(`The ${what}'s id`);
7
8
  const projectId = id('project');
@@ -25,14 +26,13 @@ const taskFields = {
25
26
  dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
26
27
  notes: zod_1.z.string().optional(),
27
28
  };
29
+ // An asset's license metadata: what crediting it takes. Each is optional; an empty one clears it.
28
30
  const rightsFields = {
29
- rightsHolder: zod_1.z.string().optional(),
30
- licenseType: zod_1.z.string().optional().describe('e.g. CC0, CC BY 4.0, proprietary'),
31
- licenseUrl: zod_1.z.string().optional(),
32
- territory: zod_1.z.string().optional(),
33
- expiresAt: zod_1.z.string().optional().describe('An ISO 8601 date'),
34
- usageRestrictions: zod_1.z.string().optional(),
35
- notes: zod_1.z.string().optional(),
31
+ sourceUrl: zod_1.z.string().optional().describe('Where the asset was taken from (an http(s) URL)'),
32
+ creatorName: zod_1.z.string().optional().describe('Who made it'),
33
+ creatorUrl: zod_1.z.string().optional().describe("The creator's page (an http(s) URL)"),
34
+ licenseType: zod_1.z.enum(rights_1.LICENSE_TYPES).or(zod_1.z.literal('')).optional().describe('Its license'),
35
+ comments: zod_1.z.string().optional(),
36
36
  };
37
37
  // Only the fields given (an unset optional field isn't sent as undefined).
38
38
  const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
@@ -86,7 +86,7 @@ exports.TOOLS = [
86
86
  }),
87
87
  tool({
88
88
  name: 'get_asset_rights', title: "Get an asset's rights", permissions: ['rights:read'],
89
- description: "An asset's rights: holder, license, territory, expiry and restrictions.",
89
+ description: "An asset's license metadata: where it was taken from, its creator (name and link), its license and comments; null if it has none.",
90
90
  input: { projectId, assetId: id('asset') },
91
91
  projectOf: byProject,
92
92
  call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights` }),
@@ -164,7 +164,7 @@ exports.TOOLS = [
164
164
  }),
165
165
  tool({
166
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
+ description: `Sets an asset's license metadata (the fields given; an empty one clears it): where it was taken from, its creator (name and link), its license (one of: ${rights_1.LICENSE_TYPES.join(', ')}) and comments. It shows in the file's Rights tab, in Storage, and in every credits sheet the asset is in.`,
168
168
  input: { projectId, assetId: id('asset'), ...rightsFields },
169
169
  projectOf: byProject,
170
170
  call: ({ projectId, assetId, ...rights }) => ({ method: 'PUT', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights`, body: defined(rights) }),
@@ -245,6 +245,7 @@ exports.TOOLS = [
245
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
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
247
  "Categories: images, audios, videos, 3d-models (.glb, .gltf, .obj, .fbx, .stl, .ply); contentType must match it.",
248
+ "For a new asset taken from someone else (a CC model, a stock image), give its license metadata too, so it's credited (it needs the rights:write permission); for a new version, set_asset_rights changes it.",
248
249
  ].join(' '),
249
250
  input: {
250
251
  projectId,
@@ -253,10 +254,14 @@ exports.TOOLS = [
253
254
  contentType: zod_1.z.string().min(1).describe('Its MIME type (e.g. model/gltf-binary, image/png, video/mp4)'),
254
255
  assetId: zod_1.z.string().optional().describe('An existing asset to upload a new version of; a new asset if not given'),
255
256
  connectionId: zod_1.z.string().optional().describe("A storage connection to store it in; the project's built-in storage if not given"),
257
+ ...rightsFields,
256
258
  },
257
259
  projectOf: byProject,
258
- call: ({ projectId, category, fileName, contentType, assetId, connectionId }) => {
259
- const query = new URLSearchParams({ fileName, contentType, ...(connectionId && !assetId ? { category } : {}) });
260
+ call: ({ projectId, category, fileName, contentType, assetId, connectionId, ...rights }) => {
261
+ const query = new URLSearchParams({
262
+ fileName, contentType, ...(connectionId && !assetId ? { category } : {}),
263
+ ...(assetId ? {} : defined(rights)),
264
+ });
260
265
  const files = connectionId ? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}` : `/projects/${enc(projectId)}/storage`;
261
266
  const path = assetId
262
267
  ? `${files}/files/${enc(assetId)}/replace-url`
@@ -358,6 +363,15 @@ exports.TOOLS = [
358
363
  call: ({ projectId, jobId }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/jobs/${enc(jobId)}/retry` }),
359
364
  pick: (data) => data?.job,
360
365
  }),
366
+ // ── Credits sheets ──
367
+ tool({
368
+ name: 'get_credits_sheet', title: 'Read a credits sheet', permissions: ['rights:read'],
369
+ description: "A credits sheet (a document of kind 'credits', made by a Credits sheet node of the workflow; list_documents lists them): a row per asset fed to it (picked on it, in a Storage folder picked, or imported by an Ingest node connected to it) with that asset's license metadata as it is now, and its status: 'complete' when it has an asset and each has its creator, source and license; 'incomplete' otherwise, each row listing what it lacks (`missing`). To fill a row in, set_asset_rights on its asset.",
370
+ input: { projectId, documentId: id('credits sheet') },
371
+ projectOf: byProject,
372
+ call: ({ projectId, documentId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/credits-sheets/${enc(documentId)}` }),
373
+ pick: (data) => data && { id: data.document?.id, title: data.document?.title, status: data.status, rows: data.rows },
374
+ }),
361
375
  // ── Documents ──
362
376
  tool({
363
377
  name: 'list_documents', title: 'List documents', permissions: ['docs:read'],
@@ -9,6 +9,7 @@ const crypto_1 = __importDefault(require("crypto"));
9
9
  const manager_1 = require("../models/manager");
10
10
  const auth_1 = require("../utils/auth");
11
11
  const auth_2 = require("../services/auth");
12
+ const workflowResources_1 = require("../services/workflowResources");
12
13
  // The node types that make a workflow's production section (as the UI groups them).
13
14
  const PRODUCTION_NODE_TYPES = new Set(['shotNode', 'assemblyNode', 'masterNode']);
14
15
  /** A Shot node, as the UI makes one dropped on the canvas without a position. */
@@ -32,14 +33,19 @@ async function ensureWorkflowNode(requirement, projectId, auth, api) {
32
33
  const project = await (0, manager_1.getModel)('projects').findOne({ _id: projectId });
33
34
  if (!project || !(0, auth_1.activeMemberOf)(project, auth.userId))
34
35
  return null;
35
- const schema = project.workflow?.nodeSchema ?? null;
36
- const nodes = Array.isArray(schema?.nodes) ? schema.nodes : [];
36
+ // The workflow as it's answered (brought to define the project, if it wasn't yet), and saved
37
+ // on that version: what changed since isn't undone.
38
+ const workflow = await (0, workflowResources_1.readWorkflow)(project);
39
+ const nodes = Array.isArray(workflow?.nodeSchema?.nodes) ? workflow.nodeSchema.nodes : [];
37
40
  if (nodes.some(node => PRODUCTION_NODE_TYPES.has(node?.type)))
38
41
  return null;
39
42
  const answer = await api({
40
43
  method: 'PATCH',
41
44
  path: `/projects/${encodeURIComponent(projectId)}/workflow`,
42
- body: { nodeSchema: { ...(schema ?? {}), nodes: [...nodes, (0, exports.newShotNode)()], edges: Array.isArray(schema?.edges) ? schema.edges : [] } },
45
+ body: {
46
+ nodeSchema: { nodes: [...nodes, (0, exports.newShotNode)()], edges: Array.isArray(workflow?.nodeSchema?.edges) ? workflow.nodeSchema.edges : [] },
47
+ baseUpdatedAt: new Date(workflow.updatedAt).toISOString(),
48
+ },
43
49
  });
44
50
  if (answer.status >= 200 && answer.status < 300)
45
51
  return null;
@@ -45,6 +45,7 @@ const workerCallback_1 = require("./routes/workerCallback");
45
45
  const workerJobs_1 = require("../services/workerJobs");
46
46
  const deliverable_1 = require("./routes/deliverable");
47
47
  const document_1 = require("./routes/document");
48
+ const projectResources_1 = require("./routes/projectResources");
48
49
  const documents_1 = require("../services/documents");
49
50
  function createStreamByRouter(config) {
50
51
  const router = express_1.default.Router();
@@ -153,5 +154,6 @@ function createStreamByRouter(config) {
153
154
  router.use((0, worker_1.workerRouter)({ ...config, adapter }));
154
155
  router.use((0, deliverable_1.deliverableRouter)({ ...config, adapter }));
155
156
  router.use((0, document_1.documentRouter)(config));
157
+ router.use((0, projectResources_1.projectResourcesRouter)(config));
156
158
  return router;
157
159
  }
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.connectionRouter = connectionRouter;
37
37
  const express_1 = require("express");
38
+ const workflowResources_1 = require("../../services/workflowResources");
38
39
  const manager_1 = require("../../models/manager");
39
40
  const auth_1 = require("../../utils/auth");
40
41
  const connection_1 = require("../../services/connection");
@@ -88,7 +89,8 @@ function connectionRouter(config) {
88
89
  ...(description !== undefined && { description }),
89
90
  ...(token !== undefined && { token }),
90
91
  });
91
- return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection) });
92
+ await (0, workflowResources_1.addToWorkflow)(projectId, 'api', connection);
93
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection), connections: (0, sanitize_1.sanitizeConnections)((await Project.findOne({ _id: projectId }))?.connections) });
92
94
  }
93
95
  catch (err) {
94
96
  res.status(500).json({ message: 'Failed to add connection', details: err.message });
@@ -178,7 +180,8 @@ function connectionRouter(config) {
178
180
  return res.status(404).json({ message: 'Connection not found' });
179
181
  }
180
182
  await (0, connection_1.deleteConnection)(config, projectId, connectionId);
181
- return res.status(200).json({ message: 'Connection deleted successfully.' });
183
+ await (0, workflowResources_1.removeFromWorkflow)(projectId, 'api', connectionId);
184
+ return res.status(200).json({ message: 'Connection deleted successfully.', connections: (0, sanitize_1.sanitizeConnections)((await Project.findOne({ _id: projectId }))?.connections) });
182
185
  }
183
186
  catch (err) {
184
187
  res.status(500).json({ message: 'Failed to delete connection', details: err.message });
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dbConnectionRouter = dbConnectionRouter;
4
4
  const express_1 = require("express");
5
+ const workflowResources_1 = require("../../services/workflowResources");
6
+ const projectResources_1 = require("./projectResources");
7
+ const projectIntegration_1 = require("../../services/projectIntegration");
5
8
  const mongodb_1 = require("mongodb");
6
9
  const manager_1 = require("../../models/manager");
7
10
  const auth_1 = require("../../utils/auth");
@@ -45,11 +48,29 @@ function dbConnectionRouter(config) {
45
48
  res.status(500).json({ message: 'Failed to fetch DB connections', details: err.message });
46
49
  }
47
50
  });
51
+ // The project's database connections as they're answered, after each change too.
52
+ const listNow = async (projectId) => (0, sanitize_1.sanitizeConnections)((await Project.findOne({ _id: projectId }))?.dbConnections);
48
53
  // ─── Add DB connection ────────────────────────────────────────────────────
49
54
  router.post('/projects/:id/connections/db', async (req, res) => {
50
55
  try {
51
56
  const auth = req.auth;
52
57
  const { name, dbType, credential, integrationId, description } = req.body;
58
+ if (credential && integrationId) {
59
+ return res.status(400).json({ message: 'Provide only one of credential or integrationId' });
60
+ }
61
+ // An integration (the user's, or a built-in) is a named, typed database already.
62
+ if (integrationId) {
63
+ const project = await Project.findOne({ _id: req.params.id });
64
+ if (!project)
65
+ return res.status(404).json({ message: 'Project not found' });
66
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
67
+ return res.status(403).json({ message: 'Unauthorized project access' });
68
+ const result = await (0, projectIntegration_1.connectIntegration)(project, integrationId, auth, config, { kind: 'database', description });
69
+ if ('error' in result)
70
+ return res.status(result.status).json({ message: result.error });
71
+ await (0, workflowResources_1.addToWorkflow)(req.params.id, 'db', result.connection);
72
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(result.connection), dbConnections: await listNow(req.params.id) });
73
+ }
53
74
  if (!name || !dbType) {
54
75
  return res.status(400).json({ message: 'name and dbType are required' });
55
76
  }
@@ -93,7 +114,8 @@ function dbConnectionRouter(config) {
93
114
  ...(description !== undefined && { description }),
94
115
  };
95
116
  await Project.update({ _id: req.params.id }, { $push: { dbConnections: connection } });
96
- return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection) });
117
+ await (0, workflowResources_1.addToWorkflow)(req.params.id, 'db', connection);
118
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection), dbConnections: await listNow(req.params.id) });
97
119
  }
98
120
  catch (err) {
99
121
  res.status(500).json({ message: 'Failed to add DB connection', details: err.message });
@@ -111,11 +133,13 @@ function dbConnectionRouter(config) {
111
133
  const conn = (0, connectionResolver_1.findDbConnection)(project, req.params.connId);
112
134
  if (!conn)
113
135
  return res.status(404).json({ message: 'DB connection not found' });
114
- if (conn.source === 'builtin') {
115
- return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
116
- }
136
+ // A built-in is disconnected, not deleted: what the project keeps in it stays, and it can
137
+ // be connected again (routes/projectResources).
117
138
  await Project.update({ _id: req.params.id }, { $pull: { dbConnections: { id: req.params.connId } } });
118
- return res.status(200).json({ message: 'DB connection deleted successfully' });
139
+ if (conn.source === 'builtin')
140
+ await (0, projectResources_1.rememberDisconnectedBuiltin)(req.params.id, 'db', conn);
141
+ await (0, workflowResources_1.removeFromWorkflow)(req.params.id, 'db', req.params.connId);
142
+ return res.status(200).json({ message: 'DB connection deleted successfully', dbConnections: await listNow(req.params.id) });
119
143
  }
120
144
  catch (err) {
121
145
  res.status(500).json({ message: 'Failed to delete DB connection', details: err.message });
@@ -11,6 +11,7 @@ const auth_1 = require("../../utils/auth");
11
11
  const wsHub_1 = require("../../services/wsHub");
12
12
  const documents_1 = require("../../services/documents");
13
13
  const auth_2 = require("../../services/auth");
14
+ const creditsSheets_1 = require("../../services/creditsSheets");
14
15
  const MAX_TITLE = 200;
15
16
  const MAX_CONTENT = 1000000;
16
17
  // What happened to a project's documents, to whoever is in it now: a view of a document
@@ -30,7 +31,9 @@ function checkTitle(value) {
30
31
  return { error: `A title has at most ${MAX_TITLE} characters` };
31
32
  return value.trim();
32
33
  }
33
- function checkContent(value) {
34
+ function checkContent(value, kind = 'document') {
35
+ if (kind === 'credits')
36
+ return (0, creditsSheets_1.checkSheetContent)(value);
34
37
  if (typeof value !== 'string')
35
38
  return { error: 'content must be text (markdown)' };
36
39
  if (value.length > MAX_CONTENT)
@@ -92,15 +95,15 @@ function documentRouter(_config) {
92
95
  if (!found)
93
96
  return;
94
97
  const { project, auth, projectId } = found;
95
- const kind = req.body?.kind === 'folder' ? 'folder' : 'document';
96
- if (req.body?.kind !== undefined && !['document', 'folder'].includes(req.body.kind))
97
- return res.status(400).json({ message: "kind is 'document' or 'folder'" });
98
+ if (req.body?.kind !== undefined && !['document', 'folder', 'credits'].includes(req.body.kind))
99
+ return res.status(400).json({ message: "kind is 'document', 'folder' or 'credits'" });
100
+ const kind = req.body?.kind ?? 'document';
98
101
  const title = checkTitle(req.body?.title);
99
102
  if (failed(title))
100
103
  return res.status(400).json({ message: title.error });
101
104
  if (kind === 'folder' && req.body?.content)
102
105
  return res.status(400).json({ message: 'A folder has no content' });
103
- const content = kind === 'folder' ? '' : checkContent(req.body?.content ?? '');
106
+ const content = kind === 'folder' ? '' : checkContent(req.body?.content ?? '', kind);
104
107
  if (failed(content))
105
108
  return res.status(400).json({ message: content.error });
106
109
  const store = (0, documents_1.documentStore)();
@@ -168,7 +171,7 @@ function documentRouter(_config) {
168
171
  return res.status(400).json({ message: title.error });
169
172
  if (current.kind === 'folder' && body.content)
170
173
  return res.status(400).json({ message: 'A folder has no content' });
171
- const content = body.content === undefined ? current.content : checkContent(body.content);
174
+ const content = body.content === undefined ? current.content : checkContent(body.content, current.kind);
172
175
  if (failed(content))
173
176
  return res.status(400).json({ message: content.error });
174
177
  text = { title, content };
@@ -240,6 +243,8 @@ function documentRouter(_config) {
240
243
  return res.status(404).json({ message: 'Document not found' });
241
244
  if (current.kind === 'folder')
242
245
  return res.status(400).json({ message: 'A folder has no content' });
246
+ if (current.kind === 'credits')
247
+ return res.status(400).json({ message: 'A credits sheet has no sections: its content is its sources' });
243
248
  // A section of another version than the one it was read on could be another text.
244
249
  if (current.version !== baseVersion)
245
250
  return conflict(res, current);
@@ -1,3 +1,31 @@
1
1
  import { Router } from 'express';
2
2
  import { StreamByConfig } from '../../types';
3
+ export declare const DEFAULT_NODE_SCHEMA: {
4
+ nodes: {
5
+ id: string;
6
+ type: string;
7
+ position: {
8
+ x: number;
9
+ y: number;
10
+ };
11
+ data: {
12
+ label: string;
13
+ subtitle: string;
14
+ };
15
+ width: number;
16
+ height: number;
17
+ }[];
18
+ edges: {
19
+ id: string;
20
+ source: string;
21
+ sourceHandle: string;
22
+ target: string;
23
+ targetHandle: string;
24
+ animated: boolean;
25
+ style: {
26
+ stroke: string;
27
+ strokeWidth: number;
28
+ };
29
+ }[];
30
+ };
3
31
  export declare function exportRouter(config: StreamByConfig): Router;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_NODE_SCHEMA = void 0;
3
4
  exports.exportRouter = exportRouter;
4
5
  const express_1 = require("express");
6
+ const workflowResources_1 = require("../../services/workflowResources");
5
7
  const deliverables_1 = require("../../services/deliverables");
6
8
  const sanitize_1 = require("../../utils/sanitize");
7
9
  const manager_1 = require("../../models/manager");
@@ -11,7 +13,7 @@ const executor_1 = require("../../services/executor");
11
13
  const connectionManager_1 = require("../../adapters/database/connectionManager");
12
14
  const mongodb_1 = require("mongodb");
13
15
  const encryption_1 = require("../../utils/encryption");
14
- const DEFAULT_NODE_SCHEMA = {
16
+ exports.DEFAULT_NODE_SCHEMA = {
15
17
  nodes: [
16
18
  {
17
19
  id: 'request',
@@ -251,7 +253,7 @@ function exportRouter(config) {
251
253
  if (!name) {
252
254
  return res.status(400).json({ message: 'Missing export name' });
253
255
  }
254
- const resolvedNodeSchema = nodeSchema ?? DEFAULT_NODE_SCHEMA;
256
+ const resolvedNodeSchema = nodeSchema ?? exports.DEFAULT_NODE_SCHEMA;
255
257
  const exportType = useConnections ? 'externalApi' : 'json';
256
258
  const project = await Project.findOne({ _id: projectId });
257
259
  if (!project || !(0, auth_1.canEditProject)(project, auth.userId)) {
@@ -260,7 +262,8 @@ function exportRouter(config) {
260
262
  const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
261
263
  const dbType = mainDb?.type ?? 'nosql';
262
264
  const result = await (0, export_1.createExport)(config, projectId, description, name, dbType, exportType, isPrivate, allowedOrigin, resolvedNodeSchema, useConnections, useCredentials, storageDbId);
263
- res.status(201).json({ data: result, message: result.message });
265
+ await (0, workflowResources_1.addToWorkflow)(projectId, 'export', { id: result.exportId, name, method: 'GET' });
266
+ res.status(201).json({ data: result, exports: (0, sanitize_1.exportSummaries)((await Project.findOne({ _id: projectId }))?.exports), message: result.message });
264
267
  }
265
268
  catch (err) {
266
269
  res.status(500).json({ message: 'Failed to create raw export', details: err.message });
@@ -310,7 +313,8 @@ function exportRouter(config) {
310
313
  }
311
314
  await (0, export_1.deleteExport)(config, projectId, exportId, project.dbType);
312
315
  await (0, deliverables_1.deleteDeliverablesOf)(projectId, { exportId });
313
- res.status(200).json({ message: 'Export deleted successfully' });
316
+ await (0, workflowResources_1.removeFromWorkflow)(projectId, 'export', exportId);
317
+ res.status(200).json({ message: 'Export deleted successfully', exports: (0, sanitize_1.exportSummaries)((await Project.findOne({ _id: projectId }))?.exports) });
314
318
  }
315
319
  catch (err) {
316
320
  res.status(500).json({ message: 'Failed to delete export', details: err.message });