streamby-core 0.42.0 → 0.43.1

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 (39) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +92 -4
  3. package/dist/adapters/database/connectionManager.js +4 -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/member.js +24 -5
  15. package/dist/middleware/routes/pipeline.js +5 -2
  16. package/dist/middleware/routes/project.js +14 -12
  17. package/dist/middleware/routes/projectResources.d.ts +8 -0
  18. package/dist/middleware/routes/projectResources.js +51 -0
  19. package/dist/middleware/routes/rights.js +72 -52
  20. package/dist/middleware/routes/storage.js +35 -2
  21. package/dist/middleware/routes/storageConnection.js +38 -15
  22. package/dist/middleware/routes/userIntegration.js +42 -1
  23. package/dist/middleware/routes/workflow.js +120 -37
  24. package/dist/models/Model.js +13 -21
  25. package/dist/services/apiTokenPermissions.js +2 -0
  26. package/dist/services/creditsSheets.d.ts +39 -0
  27. package/dist/services/creditsSheets.js +129 -0
  28. package/dist/services/dependents.d.ts +11 -0
  29. package/dist/services/dependents.js +81 -0
  30. package/dist/services/production.d.ts +1 -1
  31. package/dist/services/production.js +2 -2
  32. package/dist/services/projectIntegration.d.ts +14 -0
  33. package/dist/services/projectIntegration.js +32 -0
  34. package/dist/services/rights.d.ts +44 -0
  35. package/dist/services/rights.js +123 -0
  36. package/dist/services/workflowResources.d.ts +90 -0
  37. package/dist/services/workflowResources.js +288 -0
  38. package/dist/types/index.d.ts +6 -1
  39. package/package.json +1 -1
@@ -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 });
@@ -157,9 +157,19 @@ function memberRouter(config) {
157
157
  if (member.status !== 'pending') {
158
158
  return res.status(400).json({ message: 'Invitation is no longer pending' });
159
159
  }
160
- const updatedMembers = project.members.map((m) => m.userId?.toString() === targetUserId ? { ...m, status: 'active' } : m);
161
- await Project.update({ _id: projectId }, { members: updatedMembers });
162
- await (0, notification_1.createNotification)(member.invitedBy, 'member_accepted', `${member.username} aceptó la invitación al proyecto "${project.name}"`, { projectId, userId: targetUserId });
160
+ if (project.dbType === 'sql') {
161
+ // A SQL project's members are rows of project_members, not a field of the project.
162
+ const connection = getSqlConnection();
163
+ if (!connection) {
164
+ return res.status(503).json({ message: 'SQL connection not available' });
165
+ }
166
+ await sql_1.sqlAdapter.update(connection, 'project_members', { projectId, userId: targetUserId }, { status: 'active' }, 'streamby');
167
+ }
168
+ else {
169
+ const updatedMembers = project.members.map((m) => m.userId?.toString() === targetUserId ? { ...m, status: 'active' } : m);
170
+ await Project.update({ _id: projectId }, { members: updatedMembers });
171
+ }
172
+ await (0, notification_1.createNotification)(member.invitedBy, 'member_accepted', `${member.username ?? auth.username} aceptó la invitación al proyecto "${project.name}"`, { projectId, userId: targetUserId });
163
173
  res.status(200).json({ message: 'Invitation accepted' });
164
174
  }
165
175
  catch (err) {
@@ -184,8 +194,17 @@ function memberRouter(config) {
184
194
  if (member.status !== 'pending') {
185
195
  return res.status(400).json({ message: 'Invitation is no longer pending' });
186
196
  }
187
- await Project.update({ _id: projectId }, { $pull: { members: { userId: targetUserId } } });
188
- await (0, notification_1.createNotification)(member.invitedBy, 'member_rejected', `${member.username} rechazó la invitación al proyecto "${project.name}"`, { projectId, userId: targetUserId });
197
+ if (project.dbType === 'sql') {
198
+ const connection = getSqlConnection();
199
+ if (!connection) {
200
+ return res.status(503).json({ message: 'SQL connection not available' });
201
+ }
202
+ await sql_1.sqlAdapter.delete(connection, 'project_members', { projectId, userId: targetUserId }, 'streamby');
203
+ }
204
+ else {
205
+ await Project.update({ _id: projectId }, { $pull: { members: { userId: targetUserId } } });
206
+ }
207
+ await (0, notification_1.createNotification)(member.invitedBy, 'member_rejected', `${member.username ?? auth.username} rechazó la invitación al proyecto "${project.name}"`, { projectId, userId: targetUserId });
189
208
  res.status(200).json({ message: 'Invitation rejected' });
190
209
  }
191
210
  catch (err) {
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.pipelineRouter = pipelineRouter;
7
7
  const express_1 = require("express");
8
+ const workflowResources_1 = require("../../services/workflowResources");
8
9
  const auth_1 = require("../../utils/auth");
9
10
  const connectionManager_1 = require("../../adapters/database/connectionManager");
10
11
  const manager_1 = require("../../models/manager");
@@ -86,7 +87,8 @@ function pipelineRouter(_config) {
86
87
  };
87
88
  await db.collection('pipelines').insertOne({ ...pipeline });
88
89
  await syncProjectRefs(projectId);
89
- res.status(201).json({ pipeline });
90
+ await (0, workflowResources_1.addToWorkflow)(projectId, 'pipeline', pipeline);
91
+ res.status(201).json({ pipeline, pipelines: (await Project.findOne({ _id: projectId }))?.pipelines ?? [] });
90
92
  }
91
93
  catch (err) {
92
94
  res.status(500).json({ message: err.message });
@@ -161,7 +163,8 @@ function pipelineRouter(_config) {
161
163
  return res.status(500).json({ message: 'Database not available' });
162
164
  await db.collection('pipelines').deleteOne({ id: pipelineId, projectId });
163
165
  await syncProjectRefs(projectId);
164
- res.json({ message: 'Pipeline deleted' });
166
+ await (0, workflowResources_1.removeFromWorkflow)(projectId, 'pipeline', pipelineId);
167
+ res.json({ message: 'Pipeline deleted', pipelines: (await Project.findOne({ _id: projectId }))?.pipelines ?? [] });
165
168
  }
166
169
  catch (err) {
167
170
  res.status(500).json({ message: err.message });
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.projectRouter = projectRouter;
4
4
  const express_1 = require("express");
5
+ const workflowResources_1 = require("../../services/workflowResources");
5
6
  const manager_1 = require("../../models/manager");
6
7
  const auth_1 = require("../../utils/auth");
7
8
  const connectionManager_1 = require("../../adapters/database/connectionManager");
8
9
  const sql_1 = require("../../adapters/database/sql");
9
10
  const sanitize_1 = require("../../utils/sanitize");
10
11
  const projectIntegration_1 = require("../../services/projectIntegration");
12
+ const projectResources_1 = require("./projectResources");
11
13
  const auth_2 = require("../../services/auth");
12
14
  const apiTokens_1 = require("../../services/apiTokens");
13
15
  const nosqlDb_1 = require("../../utils/nosqlDb");
@@ -445,18 +447,11 @@ function projectRouter(config) {
445
447
  return res.status(404).json({ message: 'Project not found' });
446
448
  if (!(0, auth_1.canEditProject)(project, auth.userId))
447
449
  return res.status(403).json({ message: 'Unauthorized project access' });
448
- const alreadyConnected = [...(project.dbConnections ?? []), ...(project.storageConnections ?? []), ...(project.integrationConnections ?? [])]
449
- .some((c) => c.integrationId === integrationId);
450
- if (alreadyConnected)
451
- return res.status(400).json({ message: 'This integration is already connected to the project' });
452
- const result = await (0, projectIntegration_1.classifyIntegrationId)(integrationId, auth, config, projectId);
450
+ const result = await (0, projectIntegration_1.connectIntegration)(project, integrationId, auth, config);
453
451
  if ('error' in result)
454
452
  return res.status(result.status).json({ message: result.error });
455
- const updated = result.kind === 'database'
456
- ? await Project.update({ _id: projectId }, { $push: { dbConnections: result.connection } })
457
- : result.kind === 'storage'
458
- ? await Project.update({ _id: projectId }, { $push: { storageConnections: result.connection } })
459
- : await Project.update({ _id: projectId }, { $push: { integrationConnections: result.connection } });
453
+ await (0, workflowResources_1.addToWorkflow)(projectId, result.kind === 'database' ? 'db' : result.kind, result.connection);
454
+ const updated = await Project.findOne({ _id: projectId });
460
455
  if (!updated)
461
456
  return res.status(404).json({ message: 'Project not found or not updated' });
462
457
  res.status(201).json({ data: result.connection, project: (0, sanitize_1.sanitizeProject)(updated) });
@@ -481,13 +476,20 @@ function projectRouter(config) {
481
476
  const integrationConn = (project.integrationConnections ?? []).find((c) => c.integrationId === integrationId);
482
477
  if (!dbConn && !storageConn && !integrationConn)
483
478
  return res.status(404).json({ message: 'Integration not connected to this project' });
484
- const updated = dbConn
479
+ const removed = dbConn
485
480
  ? await Project.update({ _id: projectId }, { $pull: { dbConnections: { id: dbConn.id } } })
486
481
  : storageConn
487
482
  ? await Project.update({ _id: projectId }, { $pull: { storageConnections: { id: storageConn.id } } })
488
483
  : await Project.update({ _id: projectId }, { $pull: { integrationConnections: { id: integrationConn.id } } });
489
- if (!updated)
484
+ if (!removed)
490
485
  return res.status(404).json({ message: 'Project not found or not updated' });
486
+ // A built-in disconnected keeps its id, to come back as it was (services/projectIntegration).
487
+ const builtin = [dbConn, storageConn].find(c => c?.source === 'builtin');
488
+ if (builtin)
489
+ await (0, projectResources_1.rememberDisconnectedBuiltin)(projectId, dbConn ? 'db' : 'storage', builtin);
490
+ const gone = dbConn ?? storageConn ?? integrationConn;
491
+ await (0, workflowResources_1.removeFromWorkflow)(projectId, dbConn ? 'db' : storageConn ? 'storage' : 'service', gone.id);
492
+ const updated = await Project.findOne({ _id: projectId });
491
493
  res.status(200).json({ message: 'Integration removed', project: (0, sanitize_1.sanitizeProject)(updated) });
492
494
  }
493
495
  catch (err) {
@@ -0,0 +1,8 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function projectResourcesRouter(_config: StreamByConfig): Router;
4
+ /** A built-in disconnected: its id kept, to connect it again as it was (services/projectIntegration). */
5
+ export declare function rememberDisconnectedBuiltin(projectId: string, kind: 'db' | 'storage', conn: {
6
+ id: string;
7
+ integrationId?: string;
8
+ }): Promise<void>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectResourcesRouter = projectResourcesRouter;
4
+ exports.rememberDisconnectedBuiltin = rememberDisconnectedBuiltin;
5
+ const express_1 = require("express");
6
+ const manager_1 = require("../../models/manager");
7
+ const auth_1 = require("../../utils/auth");
8
+ const dependents_1 = require("../../services/dependents");
9
+ // A project's resources as a whole: what depends on one, asked before removing it.
10
+ function projectResourcesRouter(_config) {
11
+ const router = (0, express_1.Router)();
12
+ const Project = (0, manager_1.getModel)('projects');
13
+ async function projectFor(req, res, edit = false) {
14
+ const auth = req.auth;
15
+ const project = await Project.findOne({ _id: req.params.id });
16
+ if (!project) {
17
+ res.status(404).json({ message: 'Project not found' });
18
+ return null;
19
+ }
20
+ if (!(edit ? auth_1.canEditProject : auth_1.isProjectMember)(project, auth.userId)) {
21
+ res.status(403).json({ message: 'Unauthorized project access' });
22
+ return null;
23
+ }
24
+ return { project, auth };
25
+ }
26
+ router.get('/projects/:id/dependents/:kind/:resourceId', async (req, res) => {
27
+ try {
28
+ const found = await projectFor(req, res);
29
+ if (!found)
30
+ return;
31
+ const kind = req.params.kind;
32
+ if (!dependents_1.RESOURCE_KINDS.includes(kind))
33
+ return res.status(400).json({ message: `kind is one of: ${dependents_1.RESOURCE_KINDS.join(', ')}` });
34
+ if (!(0, dependents_1.findResource)(found.project, kind, req.params.resourceId))
35
+ return res.status(404).json({ message: 'Not found in this project' });
36
+ res.json({ dependents: await (0, dependents_1.dependentsOf)(found.project, kind, req.params.resourceId) });
37
+ }
38
+ catch (err) {
39
+ res.status(500).json({ message: err.message });
40
+ }
41
+ });
42
+ return router;
43
+ }
44
+ /** A built-in disconnected: its id kept, to connect it again as it was (services/projectIntegration). */
45
+ async function rememberDisconnectedBuiltin(projectId, kind, conn) {
46
+ if (!conn.integrationId)
47
+ return;
48
+ const Project = (0, manager_1.getModel)('projects');
49
+ await Project.update({ _id: projectId }, { $pull: { disconnectedBuiltins: { integrationId: conn.integrationId } } });
50
+ await Project.update({ _id: projectId }, { $push: { disconnectedBuiltins: { kind, integrationId: conn.integrationId, connectionId: conn.id } } });
51
+ }
@@ -3,74 +3,94 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.rightsRouter = rightsRouter;
4
4
  const express_1 = require("express");
5
5
  const auth_1 = require("../../utils/auth");
6
- const connectionManager_1 = require("../../adapters/database/connectionManager");
7
6
  const manager_1 = require("../../models/manager");
8
- function getDb() {
9
- const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
10
- if (!ids.length)
11
- return null;
12
- return (0, connectionManager_1.getConnection)(ids[0]).client.db();
13
- }
7
+ const nosqlDb_1 = require("../../utils/nosqlDb");
8
+ const rights_1 = require("../../services/rights");
9
+ const documents_1 = require("../../services/documents");
10
+ const creditsSheets_1 = require("../../services/creditsSheets");
14
11
  function rightsRouter(_config) {
15
12
  const router = (0, express_1.Router)();
16
13
  const Project = (0, manager_1.getModel)('projects');
17
- // GET /projects/:projectId/assets/:assetId/rights
14
+ // The project, if the caller may see it (or, with `edit`, change it); otherwise the answer.
15
+ async function projectFor(req, res, edit = false) {
16
+ const auth = req.auth;
17
+ const project = await Project.findOne({ _id: req.params.projectId });
18
+ if (!project) {
19
+ res.status(404).json({ message: 'Project not found' });
20
+ return null;
21
+ }
22
+ if (!(edit ? auth_1.canEditProject : auth_1.isProjectMember)(project, auth.userId)) {
23
+ res.status(403).json({ message: 'Unauthorized' });
24
+ return null;
25
+ }
26
+ return { project, auth, projectId: String(req.params.projectId) };
27
+ }
18
28
  router.get('/projects/:projectId/assets/:assetId/rights', async (req, res) => {
19
29
  try {
20
- const auth = req.auth;
21
- const { projectId, assetId } = req.params;
22
- const project = await Project.findOne({ _id: projectId });
23
- if (!project)
24
- return res.status(404).json({ message: 'Project not found' });
25
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
26
- return res.status(403).json({ message: 'Unauthorized' });
27
- const db = getDb();
28
- if (!db)
30
+ const found = await projectFor(req, res);
31
+ if (!found)
32
+ return;
33
+ if (!(0, nosqlDb_1.nosqlDb)())
29
34
  return res.status(500).json({ message: 'Database not available' });
30
- const rights = await db.collection('asset_rights').findOne({ assetId, projectId });
31
- res.json({ rights: rights ?? null });
35
+ res.json({ rights: await (0, rights_1.getRights)(found.projectId, req.params.assetId) });
32
36
  }
33
37
  catch (err) {
34
38
  res.status(500).json({ message: err.message });
35
39
  }
36
40
  });
37
- // PUT /projects/:projectId/assets/:assetId/rights (upsert)
41
+ // Sets the fields given of the asset's license metadata (who may edit the asset may).
38
42
  router.put('/projects/:projectId/assets/:assetId/rights', async (req, res) => {
39
43
  try {
40
- const auth = req.auth;
41
- const { projectId, assetId } = req.params;
42
- const project = await Project.findOne({ _id: projectId });
43
- if (!project)
44
- return res.status(404).json({ message: 'Project not found' });
45
- if (!(0, auth_1.canEditProject)(project, auth.userId))
46
- return res.status(403).json({ message: 'Unauthorized' });
47
- const db = getDb();
44
+ const found = await projectFor(req, res, true);
45
+ if (!found)
46
+ return;
47
+ const db = (0, nosqlDb_1.nosqlDb)();
48
48
  if (!db)
49
49
  return res.status(500).json({ message: 'Database not available' });
50
- const { rightsHolder, licenseType, licenseUrl, territory, expiresAt, usageRestrictions, notes } = req.body;
51
- const now = new Date();
52
- const payload = {
53
- assetId,
54
- projectId,
55
- licenseType: licenseType ?? 'unknown',
56
- updatedBy: auth.userId,
57
- updatedAt: now,
58
- };
59
- if (rightsHolder !== undefined)
60
- payload.rightsHolder = rightsHolder;
61
- if (licenseUrl !== undefined)
62
- payload.licenseUrl = licenseUrl;
63
- if (territory !== undefined)
64
- payload.territory = territory;
65
- if (expiresAt !== undefined)
66
- payload.expiresAt = expiresAt ? new Date(expiresAt) : null;
67
- if (usageRestrictions !== undefined)
68
- payload.usageRestrictions = usageRestrictions;
69
- if (notes !== undefined)
70
- payload.notes = notes;
71
- await db.collection('asset_rights').updateOne({ assetId, projectId }, { $set: payload }, { upsert: true });
72
- const rights = await db.collection('asset_rights').findOne({ assetId, projectId });
73
- res.json({ rights });
50
+ const { assetId } = req.params;
51
+ if (!(await db.collection('storage_files').findOne({ projectId: found.projectId, fileId: assetId }))) {
52
+ return res.status(404).json({ message: 'Asset not found' });
53
+ }
54
+ const checked = (0, rights_1.checkRights)(req.body);
55
+ if ('error' in checked)
56
+ return res.status(400).json({ message: checked.error });
57
+ res.json({ rights: await (0, rights_1.saveRights)(found.project, assetId, checked.values, found.auth.userId) });
58
+ }
59
+ catch (err) {
60
+ res.status(500).json({ message: err.message });
61
+ }
62
+ });
63
+ // The project's credits sheets, each with its status (as its node shows it).
64
+ router.get('/projects/:projectId/credits-sheets', async (req, res) => {
65
+ try {
66
+ const found = await projectFor(req, res);
67
+ if (!found)
68
+ return;
69
+ const store = (0, documents_1.documentStore)();
70
+ const sheets = (await store.list(found.projectId)).filter(d => d.kind === 'credits');
71
+ const read = await Promise.all(sheets.map(async (summary) => {
72
+ const document = await store.get(found.projectId, summary.id);
73
+ if (!document)
74
+ return null;
75
+ const { rows, status } = await (0, creditsSheets_1.readSheet)(found.project, document);
76
+ return { id: document.id, title: document.title, status, rows: rows.length, incomplete: rows.filter(r => r.missing.length).length };
77
+ }));
78
+ res.json({ sheets: read.filter(Boolean) });
79
+ }
80
+ catch (err) {
81
+ res.status(500).json({ message: err.message });
82
+ }
83
+ });
84
+ // A credits sheet: its rows, made now from its sources and its assets' metadata, and its status.
85
+ router.get('/projects/:projectId/credits-sheets/:docId', async (req, res) => {
86
+ try {
87
+ const found = await projectFor(req, res);
88
+ if (!found)
89
+ return;
90
+ const document = await (0, documents_1.documentStore)().get(found.projectId, req.params.docId);
91
+ if (!document || document.kind !== 'credits')
92
+ return res.status(404).json({ message: 'Credits sheet not found' });
93
+ res.json(await (0, creditsSheets_1.readSheet)(found.project, document));
74
94
  }
75
95
  catch (err) {
76
96
  res.status(500).json({ message: err.message });
@@ -18,6 +18,8 @@ const crypto_1 = __importDefault(require("crypto"));
18
18
  const signedUrls_1 = require("../../services/signedUrls");
19
19
  const assetVersions_1 = require("../../services/assetVersions");
20
20
  const deliverables_1 = require("../../services/deliverables");
21
+ const rights_1 = require("../../services/rights");
22
+ const auth_2 = require("../../services/auth");
21
23
  const assetCategories_1 = require("../../utils/assetCategories");
22
24
  function getRawFilesCollection() {
23
25
  const model = (0, manager_1.getModel)('storage_files', 'nosql');
@@ -116,6 +118,9 @@ function storageRouter(config) {
116
118
  return res.status(404).json({ message: 'Project not found' });
117
119
  if (!(0, auth_1.canEditProject)(project, auth.userId))
118
120
  return res.status(403).json({ message: 'Unauthorized access' });
121
+ const rights = uploadRights(req, res);
122
+ if (rights === false)
123
+ return;
119
124
  if (!adapter.getPresignedUploadUrl) {
120
125
  return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
121
126
  }
@@ -137,6 +142,8 @@ function storageRouter(config) {
137
142
  };
138
143
  await collection.insertOne(fileDoc);
139
144
  await (0, assetVersions_1.recordFirstVersion)(fileDoc);
145
+ if (rights)
146
+ await (0, rights_1.saveRights)(project, fileId, rights, auth.userId);
140
147
  }
141
148
  const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
142
149
  const url = await adapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
@@ -233,6 +240,7 @@ function storageRouter(config) {
233
240
  return res.status(404).json({ message: 'File not found' });
234
241
  await deleteAsset(fileDoc, adapter);
235
242
  await collection.deleteOne({ fileId, projectId });
243
+ (0, rights_1.broadcastRights)(project, { action: 'deleted', assetIds: [fileId] });
236
244
  res.json({ message: 'File deleted successfully' });
237
245
  }
238
246
  catch (err) {
@@ -350,6 +358,9 @@ function storageRouter(config) {
350
358
  return res.status(404).json({ message: 'Project not found' });
351
359
  if (!(0, auth_1.canEditProject)(project, auth.userId))
352
360
  return res.status(403).json({ message: 'Unauthorized access' });
361
+ const rights = uploadRights(req, res);
362
+ if (rights === false)
363
+ return;
353
364
  const connAdapter = await resolveConnAdapter(connId, project, auth);
354
365
  if ('error' in connAdapter)
355
366
  return res.status(connAdapter.status).json({ message: connAdapter.error });
@@ -374,6 +385,8 @@ function storageRouter(config) {
374
385
  };
375
386
  await collection.insertOne(fileDoc);
376
387
  await (0, assetVersions_1.recordFirstVersion)(fileDoc);
388
+ if (rights)
389
+ await (0, rights_1.saveRights)(project, fileId, rights, auth.userId);
377
390
  }
378
391
  const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
379
392
  const url = await connAdapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
@@ -525,6 +538,7 @@ function storageRouter(config) {
525
538
  return res.status(404).json({ message: 'File not found' });
526
539
  await deleteAsset(fileDoc, connAdapter);
527
540
  await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
541
+ (0, rights_1.broadcastRights)(project, { action: 'deleted', assetIds: [fileId] });
528
542
  res.json({ message: 'File deleted successfully' });
529
543
  }
530
544
  catch (err) {
@@ -818,8 +832,26 @@ async function resolveFileUrl(file, adapter, expiresIn) {
818
832
  }
819
833
  // When a signed URL stops working, for a client to know (only when it's short-lived).
820
834
  const expiry = (expiresIn) => expiresIn ? { expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString() } : {};
821
- // An asset's files (every version), its versions and its deliverables. Its record is the
822
- // caller's to delete.
835
+ // The license metadata given with an upload (licenseType, sourceUrl, creatorName, creatorUrl,
836
+ // comments as query params), checked: null if none, false if it was refused (and answered).
837
+ // An API token sets it only with rights:write.
838
+ function uploadRights(req, res) {
839
+ const checked = (0, rights_1.rightsFromQuery)(req.query);
840
+ if (!checked)
841
+ return null;
842
+ const auth = req.auth;
843
+ if (auth.apiToken && !(0, auth_2.tokenAllows)(auth.apiToken, 'rights:write')) {
844
+ res.status(403).json({ error: "This API token doesn't have the 'rights:write' permission" });
845
+ return false;
846
+ }
847
+ if ('error' in checked) {
848
+ res.status(400).json({ message: checked.error });
849
+ return false;
850
+ }
851
+ return checked.values;
852
+ }
853
+ // An asset's files (every version), its versions, its deliverables and its license metadata.
854
+ // Its record is the caller's to delete.
823
855
  async function deleteAsset(fileDoc, adapter) {
824
856
  const keys = await (0, assetVersions_1.versionStorageKeys)(fileDoc.projectId, fileDoc.fileId, fileDoc.storageKey);
825
857
  if (adapter.deleteFile)
@@ -827,4 +859,5 @@ async function deleteAsset(fileDoc, adapter) {
827
859
  await adapter.deleteFile(key);
828
860
  await (0, assetVersions_1.deleteVersions)(fileDoc.projectId, fileDoc.fileId);
829
861
  await (0, deliverables_1.deleteDeliverablesOf)(fileDoc.projectId, { assetId: fileDoc.fileId });
862
+ await (0, rights_1.deleteRightsOf)(fileDoc.projectId, fileDoc.fileId);
830
863
  }
@@ -2,16 +2,29 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.storageConnectionRouter = storageConnectionRouter;
4
4
  const express_1 = require("express");
5
+ const workflowResources_1 = require("../../services/workflowResources");
5
6
  const mongodb_1 = require("mongodb");
6
7
  const manager_1 = require("../../models/manager");
7
8
  const auth_1 = require("../../utils/auth");
8
9
  const builtinAccess_1 = require("../../utils/builtinAccess");
9
10
  const sanitize_1 = require("../../utils/sanitize");
10
11
  const encryption_1 = require("../../utils/encryption");
12
+ const projectResources_1 = require("./projectResources");
13
+ const projectIntegration_1 = require("../../services/projectIntegration");
11
14
  const VALID_STORAGE_TYPES = ['s3', 'gcs', 'r2', 'azure'];
12
15
  function storageConnectionRouter(config) {
13
16
  const router = (0, express_1.Router)();
14
17
  const Project = (0, manager_1.getModel)('projects');
18
+ // The project's storage connections as they're answered, by the list and after each change.
19
+ // Availability is recomputed live for builtins — never trust a stored flag, per "validate on
20
+ // every use" (TCORE-69). BYOC (source: 'integration') is always available.
21
+ const listOf = (project, auth) => Promise.all((project.storageConnections || []).map(async (conn) => {
22
+ if (conn.source !== 'builtin')
23
+ return (0, sanitize_1.sanitizeConnection)(conn);
24
+ const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
25
+ return (0, sanitize_1.sanitizeConnection)({ ...conn, isBuiltin: true, available });
26
+ }));
27
+ const listNow = async (projectId, auth) => listOf(await Project.findOne({ _id: projectId }), auth);
15
28
  // ─── List storage connections ─────────────────────────────────────────────
16
29
  router.get('/projects/:id/connections/storage', async (req, res) => {
17
30
  try {
@@ -21,16 +34,7 @@ function storageConnectionRouter(config) {
21
34
  return res.status(404).json({ message: 'Project not found' });
22
35
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
23
36
  return res.status(403).json({ message: 'Unauthorized project access' });
24
- const connections = project.storageConnections || [];
25
- // Recompute availability live for builtins — never trust a stored flag, per
26
- // "validate on every use" (TCORE-69). BYOC (source: 'integration') is always available.
27
- const data = await Promise.all(connections.map(async (conn) => {
28
- if (conn.source !== 'builtin')
29
- return (0, sanitize_1.sanitizeConnection)(conn);
30
- const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
31
- return (0, sanitize_1.sanitizeConnection)({ ...conn, isBuiltin: true, available });
32
- }));
33
- return res.status(200).json({ data });
37
+ return res.status(200).json({ data: await listOf(project, auth) });
34
38
  }
35
39
  catch (err) {
36
40
  res.status(500).json({ message: 'Failed to fetch storage connections', details: err.message });
@@ -41,6 +45,22 @@ function storageConnectionRouter(config) {
41
45
  try {
42
46
  const auth = req.auth;
43
47
  const { name, type, credential, integrationId, description } = req.body;
48
+ if (credential && integrationId) {
49
+ return res.status(400).json({ message: 'Provide only one of credential or integrationId' });
50
+ }
51
+ // An integration (the user's, or a built-in) is a named, typed storage already.
52
+ if (integrationId) {
53
+ const project = await Project.findOne({ _id: req.params.id });
54
+ if (!project)
55
+ return res.status(404).json({ message: 'Project not found' });
56
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
57
+ return res.status(403).json({ message: 'Unauthorized project access' });
58
+ const result = await (0, projectIntegration_1.connectIntegration)(project, integrationId, auth, config, { kind: 'storage', description });
59
+ if ('error' in result)
60
+ return res.status(result.status).json({ message: result.error });
61
+ await (0, workflowResources_1.addToWorkflow)(req.params.id, 'storage', result.connection);
62
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(result.connection), storageConnections: await listNow(req.params.id, auth) });
63
+ }
44
64
  if (!name || !type) {
45
65
  return res.status(400).json({ message: 'name and type are required' });
46
66
  }
@@ -84,7 +104,8 @@ function storageConnectionRouter(config) {
84
104
  ...(description !== undefined && { description }),
85
105
  };
86
106
  await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
87
- return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection) });
107
+ await (0, workflowResources_1.addToWorkflow)(req.params.id, 'storage', connection);
108
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection), storageConnections: await listNow(req.params.id, auth) });
88
109
  }
89
110
  catch (err) {
90
111
  res.status(500).json({ message: 'Failed to add storage connection', details: err.message });
@@ -103,11 +124,13 @@ function storageConnectionRouter(config) {
103
124
  const conn = project.storageConnections?.find((c) => c.id === connId);
104
125
  if (!conn)
105
126
  return res.status(404).json({ message: 'Storage connection not found' });
106
- if (conn.source === 'builtin') {
107
- return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
108
- }
127
+ // A built-in is disconnected, not deleted: its files stay in it, and it can be connected
128
+ // again with them (routes/projectResources).
109
129
  await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
110
- return res.status(200).json({ message: 'Storage connection deleted successfully' });
130
+ if (conn.source === 'builtin')
131
+ await (0, projectResources_1.rememberDisconnectedBuiltin)(req.params.id, 'storage', conn);
132
+ await (0, workflowResources_1.removeFromWorkflow)(req.params.id, 'storage', connId);
133
+ return res.status(200).json({ message: 'Storage connection deleted successfully', storageConnections: await listNow(req.params.id, auth) });
111
134
  }
112
135
  catch (err) {
113
136
  res.status(500).json({ message: 'Failed to delete storage connection', details: err.message });