streamby-core 0.32.1 → 0.34.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 (55) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +661 -0
  3. package/README.md +53 -2
  4. package/dist/middleware/createRouter.js +17 -2
  5. package/dist/middleware/routes/ai.js +3 -3
  6. package/dist/middleware/routes/connection.js +3 -12
  7. package/dist/middleware/routes/dbConnection.js +7 -26
  8. package/dist/middleware/routes/distribution.js +12 -5
  9. package/dist/middleware/routes/export.js +5 -4
  10. package/dist/middleware/routes/media.js +23 -10
  11. package/dist/middleware/routes/member.js +3 -12
  12. package/dist/middleware/routes/{credential.d.ts → oauth.d.ts} +1 -1
  13. package/dist/middleware/routes/oauth.js +101 -0
  14. package/dist/middleware/routes/pipeline.js +3 -3
  15. package/dist/middleware/routes/production.js +9 -9
  16. package/dist/middleware/routes/project.js +24 -31
  17. package/dist/middleware/routes/renderFarm.js +13 -6
  18. package/dist/middleware/routes/review.js +4 -4
  19. package/dist/middleware/routes/rights.js +1 -1
  20. package/dist/middleware/routes/storage.js +21 -15
  21. package/dist/middleware/routes/storageConnection.js +2 -8
  22. package/dist/middleware/routes/userIntegration.js +17 -2
  23. package/dist/middleware/routes/workflow.js +1 -1
  24. package/dist/services/auth.d.ts +1 -2
  25. package/dist/services/auth.js +9 -11
  26. package/dist/services/connectionResolver.js +6 -6
  27. package/dist/services/executor.d.ts +4 -1
  28. package/dist/services/executor.js +63 -6
  29. package/dist/services/oauthClient.d.ts +10 -0
  30. package/dist/services/oauthClient.js +134 -0
  31. package/dist/services/projectIntegration.d.ts +4 -1
  32. package/dist/services/projectIntegration.js +13 -3
  33. package/dist/services/reencryptCredentials.d.ts +5 -0
  34. package/dist/services/reencryptCredentials.js +74 -0
  35. package/dist/services/userIntegration.d.ts +23 -3
  36. package/dist/services/userIntegration.js +130 -7
  37. package/dist/types/index.d.ts +31 -3
  38. package/dist/utils/auth.d.ts +15 -1
  39. package/dist/utils/auth.js +21 -6
  40. package/dist/utils/encryption.d.ts +6 -1
  41. package/dist/utils/encryption.js +80 -27
  42. package/dist/utils/sanitize.d.ts +6 -0
  43. package/dist/utils/sanitize.js +18 -1
  44. package/dist/utils/serviceEndpoints.d.ts +2 -0
  45. package/dist/utils/serviceEndpoints.js +26 -0
  46. package/package.json +4 -3
  47. package/dist/middleware/routes/apiConnection.d.ts +0 -3
  48. package/dist/middleware/routes/apiConnection.js +0 -100
  49. package/dist/middleware/routes/credential.js +0 -80
  50. package/dist/services/apiConnection.d.ts +0 -18
  51. package/dist/services/apiConnection.js +0 -65
  52. package/dist/services/credential.d.ts +0 -8
  53. package/dist/services/credential.js +0 -70
  54. package/dist/services/pipeline.d.ts +0 -2
  55. package/dist/services/pipeline.js +0 -292
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @streamby/core
2
2
 
3
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
4
+
3
5
  Middleware framework for building storage-agnostic media APIs.
4
6
  Part of the [StreamBy](https://streamby.nhexa.cl) ecosystem by TerminalCore Labs.
5
7
 
@@ -59,7 +61,7 @@ app.use('/streamby', express.json(), createStreamByRouter({
59
61
  },
60
62
  },
61
63
  ],
62
- encrypt: process.env.STREAMBY_ENCRYPTION_KEY,
64
+ encrypt: process.env.STREAMBY_ENCRYPTION_KEY, // 64 hex characters; see "Encryption and key rotation"
63
65
  websocket: { server: wss },
64
66
  }));
65
67
 
@@ -71,6 +73,51 @@ server.listen(3000);
71
73
 
72
74
  ---
73
75
 
76
+ ## 🔑 Encryption and key rotation
77
+
78
+ Credentials stored by StreamBy (connection secrets, export credentials, user integrations,
79
+ distribution and render farm credentials) are encrypted with the key you pass as `encrypt`:
80
+ 32 bytes, written as 64 hexadecimal characters. Each stored value records the id of the key
81
+ that encrypted it (a fingerprint, never the key itself), so you can rotate the key:
82
+
83
+ 1. Generate a new key, for example with `openssl rand -hex 32`.
84
+ 2. Pass the new key as `encrypt` and the previous one in `encryptPreviousKeys`.
85
+ 3. Restart. Values encrypted with a previous key still decrypt, and at startup StreamBy
86
+ re-encrypts all of them with the new key.
87
+ 4. Once that's done (the startup log says how many records it re-encrypted, and none are left
88
+ on the old key), you can remove the old key from `encryptPreviousKeys`.
89
+
90
+ ```ts
91
+ createStreamByRouter({
92
+ // ...
93
+ encrypt: process.env.STREAMBY_ENCRYPTION_KEY, // the current key
94
+ encryptPreviousKeys: [process.env.STREAMBY_PREVIOUS_KEY!], // only while rotating
95
+ });
96
+ ```
97
+
98
+ > Upgrading from a version before 0.34.0: deploy with your current key first, and rotate only
99
+ > after that deploy (see the [changelog](CHANGELOG.md)).
100
+
101
+ ---
102
+
103
+ ## 👥 Project roles
104
+
105
+ What a user can do in a project depends on their role in that project, as an active member
106
+ (an invitation counts once it's accepted). It never depends on the platform role your
107
+ `authProvider` returns.
108
+
109
+ | Role | Can |
110
+ |---|---|
111
+ | `viewer` | See the project and its content, and archive the project in their own list. |
112
+ | `editor` | Everything a viewer can, plus change the project: its details, connections, exports, files and folders, workflow, pipelines, production, reviews, rights, and start jobs. |
113
+ | `admin` | Everything an editor can, plus manage the project's members and delete the project. |
114
+
115
+ A project's public exports (`GET /streamby/:projectId/export/:exportName`) are read without a
116
+ session. They only run nodes that read data: never a node that uses an account's credential,
117
+ nor one that starts a job.
118
+
119
+ ---
120
+
74
121
  ## 🗄️ Database Schema Setup
75
122
 
76
123
  `@streamby/core` requires a pre-configured database schema for its internal operations. It does not manage database connections directly; instead, it expects the host application to provide initialized connection instances. This ensures full control and flexibility for the host environment.
@@ -279,5 +326,9 @@ Visit us [here](https://terminalcore.cl)
279
326
 
280
327
  ## 📟 License
281
328
 
282
- [MIT](./LICENSE)
329
+ Copyright (C) 2026 TerminalCore Labs
330
+
331
+ Licensed under the [GNU Affero General Public License v3.0](./LICENSE) (AGPL-3.0-only).
332
+
333
+ > **Note for consumers:** unlike most installable packages, this one is intentionally AGPL rather than MIT. AGPL's copyleft applies to any project that depends on it -- if you install `@streamby/core`, your own project must also be licensed under AGPL-3.0 (or a compatible copyleft license). Confirm this is acceptable for your use case before adding this dependency.
283
334
 
@@ -9,6 +9,7 @@ const manager_1 = require("../models/manager");
9
9
  const storage_1 = require("../providers/storage");
10
10
  const connectionManager_1 = require("../adapters/database/connectionManager");
11
11
  const backfillBuiltinConnections_1 = require("../services/backfillBuiltinConnections");
12
+ const reencryptCredentials_1 = require("../services/reencryptCredentials");
12
13
  const wsHub_1 = require("../services/wsHub");
13
14
  const auth_1 = require("../services/auth");
14
15
  const encryption_1 = require("../utils/encryption");
@@ -23,6 +24,7 @@ const connection_1 = require("./routes/connection");
23
24
  const dbConnection_1 = require("./routes/dbConnection");
24
25
  const storageConnection_1 = require("./routes/storageConnection");
25
26
  const userIntegration_1 = require("./routes/userIntegration");
27
+ const oauth_1 = require("./routes/oauth");
26
28
  const notification_1 = require("./routes/notification");
27
29
  const subscription_1 = require("./routes/subscription");
28
30
  const media_1 = require("./routes/media");
@@ -37,7 +39,7 @@ const pipeline_1 = require("./routes/pipeline");
37
39
  function createStreamByRouter(config) {
38
40
  const router = express_1.default.Router();
39
41
  if (config.encrypt)
40
- (0, encryption_1.setEncryptionKey)(config.encrypt);
42
+ (0, encryption_1.setEncryptionKey)(config.encrypt, config.encryptPreviousKeys);
41
43
  // Establish DB connections asynchronously (Postgres/Mongo + schema migrations).
42
44
  // Until they're ready, HOLD incoming requests instead of letting them through —
43
45
  // route handlers resolve projects/exports (e.g. the /menu-list export) against a
@@ -51,6 +53,16 @@ function createStreamByRouter(config) {
51
53
  .then(() => (0, backfillBuiltinConnections_1.backfillBuiltinConnections)(config))
52
54
  .catch((err) => {
53
55
  console.error('❌ StreamBy: failed to backfill built-in connections:', err);
56
+ })
57
+ .then(() => (0, reencryptCredentials_1.reencryptStoredCredentials)())
58
+ .then(({ updated, failed }) => {
59
+ if (updated)
60
+ console.log(`✅ StreamBy: re-encrypted the credentials of ${updated} record(s).`);
61
+ if (failed)
62
+ console.error(`❌ StreamBy: ${failed} stored credential(s) could not be decrypted with the configured keys; left as they are.`);
63
+ })
64
+ .catch((err) => {
65
+ console.error('❌ StreamBy: failed to re-encrypt stored credentials:', err);
54
66
  })
55
67
  .finally(() => {
56
68
  ready = true;
@@ -88,10 +100,12 @@ function createStreamByRouter(config) {
88
100
  if (config.websocket?.server) {
89
101
  (0, wsHub_1.initWsHub)(config.websocket.server, config);
90
102
  }
91
- router.use((0, user_1.userRouter)(config));
103
+ // /auth answers whether there's a session (it reads it itself); everything after needs one,
104
+ // but for the public export route (see authenticate).
92
105
  router.use((0, auth_2.authRouter)(config));
93
106
  router.use((0, auth_1.authenticate)(config));
94
107
  router.use(subscription_1.ensureSubscription);
108
+ router.use((0, user_1.userRouter)(config));
95
109
  router.use((0, export_1.exportRouter)(config));
96
110
  router.use((0, member_1.memberRouter)(config));
97
111
  router.use((0, storage_2.storageRouter)(config));
@@ -101,6 +115,7 @@ function createStreamByRouter(config) {
101
115
  router.use((0, dbConnection_1.dbConnectionRouter)(config));
102
116
  router.use((0, storageConnection_1.storageConnectionRouter)(config));
103
117
  router.use((0, userIntegration_1.userIntegrationRouter)(config));
118
+ router.use((0, oauth_1.oauthRouter)(config));
104
119
  router.use((0, notification_1.notificationRouter)(config));
105
120
  router.use((0, subscription_1.subscriptionRouter)(config));
106
121
  router.use((0, media_1.mediaRouter)(config));
@@ -29,7 +29,7 @@ function aiRouter(_config) {
29
29
  const project = await Project.findOne({ _id: projectId });
30
30
  if (!project)
31
31
  return res.status(404).json({ message: 'Project not found' });
32
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
32
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
33
33
  return res.status(403).json({ message: 'Unauthorized' });
34
34
  const job = (0, jobQueue_1.createJob)('transcription', auth.userId, projectId, { fileId, model, sourceLanguage, outputFormats, provider, credentialId });
35
35
  setImmediate(() => (0, aiProcessor_1.runTranscriptionJob)(job.jobId, fileId, projectId, { model, sourceLanguage, outputFormats, provider, credentialId }));
@@ -50,7 +50,7 @@ function aiRouter(_config) {
50
50
  const project = await Project.findOne({ _id: projectId });
51
51
  if (!project)
52
52
  return res.status(404).json({ message: 'Project not found' });
53
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
53
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
54
54
  return res.status(403).json({ message: 'Unauthorized' });
55
55
  const job = (0, jobQueue_1.createJob)('upscale', auth.userId, projectId, { fileId, scale, model, mode, credentialId });
56
56
  setImmediate(() => (0, aiProcessor_1.runUpscaleJob)(job.jobId, fileId, projectId, { scale, model, mode, credentialId }));
@@ -71,7 +71,7 @@ function aiRouter(_config) {
71
71
  const project = await Project.findOne({ _id: projectId });
72
72
  if (!project)
73
73
  return res.status(404).json({ message: 'Project not found' });
74
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
74
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
75
75
  return res.status(403).json({ message: 'Unauthorized' });
76
76
  const job = (0, jobQueue_1.createJob)('generate-asset', auth.userId, projectId, { assetType, provider, prompt, seed, credentialId });
77
77
  setImmediate(() => (0, aiProcessor_1.runGenerateAssetJob)(job.jobId, projectId, { assetType, provider, prompt, seed, credentialId }));
@@ -65,9 +65,6 @@ function connectionRouter(config) {
65
65
  router.post('/projects/:id/connections', async (req, res) => {
66
66
  try {
67
67
  const auth = req.auth;
68
- if (auth.role !== 'admin' && auth.role !== 'editor') {
69
- return res.status(403).json({ message: 'Permission denied' });
70
- }
71
68
  const projectId = req.params.id;
72
69
  const { name, apiUrl, method, description, token, prefix } = req.body;
73
70
  if (!name || !apiUrl) {
@@ -80,7 +77,7 @@ function connectionRouter(config) {
80
77
  if (!project) {
81
78
  return res.status(404).json({ message: 'Project not found' });
82
79
  }
83
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
80
+ if (!(0, auth_1.canEditProject)(project, auth.userId)) {
84
81
  return res.status(403).json({ message: 'Unauthorized project access' });
85
82
  }
86
83
  const connection = await (0, connection_1.addConnection)(config, projectId, {
@@ -101,9 +98,6 @@ function connectionRouter(config) {
101
98
  router.patch('/projects/:id/connections/:connectionId', async (req, res) => {
102
99
  try {
103
100
  const auth = req.auth;
104
- if (auth.role !== 'admin' && auth.role !== 'editor') {
105
- return res.status(403).json({ message: 'Permission denied' });
106
- }
107
101
  const { id: projectId, connectionId } = req.params;
108
102
  const { name, apiUrl, method, description, token, prefix } = req.body;
109
103
  if (method && !VALID_METHODS.includes(method)) {
@@ -112,7 +106,7 @@ function connectionRouter(config) {
112
106
  const project = await Project.findOne({ _id: projectId });
113
107
  if (!project)
114
108
  return res.status(404).json({ message: 'Project not found' });
115
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
109
+ if (!(0, auth_1.canEditProject)(project, auth.userId)) {
116
110
  return res.status(403).json({ message: 'Unauthorized project access' });
117
111
  }
118
112
  const updated = await (0, connection_1.updateConnection)(config, projectId, connectionId, {
@@ -170,16 +164,13 @@ function connectionRouter(config) {
170
164
  router.delete('/projects/:id/connections/:connectionId', async (req, res) => {
171
165
  try {
172
166
  const auth = req.auth;
173
- if (auth.role !== 'admin' && auth.role !== 'editor') {
174
- return res.status(403).json({ message: 'Permission denied' });
175
- }
176
167
  const projectId = req.params.id;
177
168
  const { connectionId } = req.params;
178
169
  const project = await Project.findOne({ _id: projectId });
179
170
  if (!project) {
180
171
  return res.status(404).json({ message: 'Project not found' });
181
172
  }
182
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
173
+ if (!(0, auth_1.canEditProject)(project, auth.userId)) {
183
174
  return res.status(403).json({ message: 'Unauthorized project access' });
184
175
  }
185
176
  const exists = project.connections?.some((c) => c.id === connectionId);
@@ -49,9 +49,6 @@ function dbConnectionRouter(config) {
49
49
  router.post('/projects/:id/connections/db', async (req, res) => {
50
50
  try {
51
51
  const auth = req.auth;
52
- if (auth.role !== 'admin' && auth.role !== 'editor') {
53
- return res.status(403).json({ message: 'Permission denied' });
54
- }
55
52
  const { name, dbType, credential, integrationId, description } = req.body;
56
53
  if (!name || !dbType) {
57
54
  return res.status(400).json({ message: 'name and dbType are required' });
@@ -68,7 +65,7 @@ function dbConnectionRouter(config) {
68
65
  const project = await Project.findOne({ _id: req.params.id });
69
66
  if (!project)
70
67
  return res.status(404).json({ message: 'Project not found' });
71
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
68
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
72
69
  return res.status(403).json({ message: 'Unauthorized project access' });
73
70
  let encryptedCredential;
74
71
  if (integrationId) {
@@ -106,13 +103,10 @@ function dbConnectionRouter(config) {
106
103
  router.delete('/projects/:id/connections/db/:connId', async (req, res) => {
107
104
  try {
108
105
  const auth = req.auth;
109
- if (auth.role !== 'admin' && auth.role !== 'editor') {
110
- return res.status(403).json({ message: 'Permission denied' });
111
- }
112
106
  const project = await Project.findOne({ _id: req.params.id });
113
107
  if (!project)
114
108
  return res.status(404).json({ message: 'Project not found' });
115
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
109
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
116
110
  return res.status(403).json({ message: 'Unauthorized project access' });
117
111
  const conn = (0, connectionResolver_1.findDbConnection)(project, req.params.connId);
118
112
  if (!conn)
@@ -149,13 +143,10 @@ function dbConnectionRouter(config) {
149
143
  router.post('/projects/:id/connections/db/:connId/tables', async (req, res) => {
150
144
  try {
151
145
  const auth = req.auth;
152
- if (auth.role !== 'admin' && auth.role !== 'editor') {
153
- return res.status(403).json({ message: 'Permission denied' });
154
- }
155
146
  const project = await Project.findOne({ _id: req.params.id });
156
147
  if (!project)
157
148
  return res.status(404).json({ message: 'Project not found' });
158
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
149
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
159
150
  return res.status(403).json({ message: 'Unauthorized project access' });
160
151
  const schema = req.body;
161
152
  if (!schema.tableName)
@@ -177,13 +168,10 @@ function dbConnectionRouter(config) {
177
168
  router.delete('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
178
169
  try {
179
170
  const auth = req.auth;
180
- if (auth.role !== 'admin' && auth.role !== 'editor') {
181
- return res.status(403).json({ message: 'Permission denied' });
182
- }
183
171
  const project = await Project.findOne({ _id: req.params.id });
184
172
  if (!project)
185
173
  return res.status(404).json({ message: 'Project not found' });
186
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
174
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
187
175
  return res.status(403).json({ message: 'Unauthorized project access' });
188
176
  await withResolvedDbClient(project, req.params.connId, config, auth, res, async (resolved) => {
189
177
  await (0, dbConnection_1.deleteTableOrCollectionInternal)(resolved.client, resolved.type, req.params.tableName, req.params.id);
@@ -218,13 +206,10 @@ function dbConnectionRouter(config) {
218
206
  router.post('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
219
207
  try {
220
208
  const auth = req.auth;
221
- if (auth.role !== 'admin' && auth.role !== 'editor') {
222
- return res.status(403).json({ message: 'Permission denied' });
223
- }
224
209
  const project = await Project.findOne({ _id: req.params.id });
225
210
  if (!project)
226
211
  return res.status(404).json({ message: 'Project not found' });
227
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
212
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
228
213
  return res.status(403).json({ message: 'Unauthorized project access' });
229
214
  const record = req.body;
230
215
  if (!record || typeof record !== 'object' || Array.isArray(record)) {
@@ -243,12 +228,10 @@ function dbConnectionRouter(config) {
243
228
  router.put('/projects/:id/connections/db/:connId/tables/:tableName/:recordId', async (req, res) => {
244
229
  try {
245
230
  const auth = req.auth;
246
- if (auth.role !== 'admin' && auth.role !== 'editor')
247
- return res.status(403).json({ message: 'Permission denied' });
248
231
  const project = await Project.findOne({ _id: req.params.id });
249
232
  if (!project)
250
233
  return res.status(404).json({ message: 'Project not found' });
251
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
234
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
252
235
  return res.status(403).json({ message: 'Unauthorized project access' });
253
236
  const updates = req.body;
254
237
  if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
@@ -267,12 +250,10 @@ function dbConnectionRouter(config) {
267
250
  router.delete('/projects/:id/connections/db/:connId/tables/:tableName/:recordId', async (req, res) => {
268
251
  try {
269
252
  const auth = req.auth;
270
- if (auth.role !== 'admin' && auth.role !== 'editor')
271
- return res.status(403).json({ message: 'Permission denied' });
272
253
  const project = await Project.findOne({ _id: req.params.id });
273
254
  if (!project)
274
255
  return res.status(404).json({ message: 'Project not found' });
275
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
256
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
276
257
  return res.status(403).json({ message: 'Unauthorized project access' });
277
258
  await withResolvedDbClient(project, req.params.connId, config, auth, res, async (resolved) => {
278
259
  await (0, dbConnection_1.deleteRecordInternal)(resolved.client, resolved.type, req.params.tableName, req.params.recordId, req.params.id);
@@ -11,6 +11,7 @@ const wsHub_1 = require("../../services/wsHub");
11
11
  const connectionManager_1 = require("../../adapters/database/connectionManager");
12
12
  const sanitize_1 = require("../../utils/sanitize");
13
13
  const crypto_1 = __importDefault(require("crypto"));
14
+ const encryption_1 = require("../../utils/encryption");
14
15
  function getDb() {
15
16
  const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
16
17
  if (!ids.length)
@@ -44,13 +45,19 @@ function distributionRouter(_config) {
44
45
  try {
45
46
  const auth = req.auth;
46
47
  const { projectId } = req.params;
47
- const { name, target, encryptedCredential, config, description } = req.body;
48
+ const { name, target, credential: credentialValue, encryptedCredential: legacyCredential, config, description } = req.body;
49
+ // The credential, as given (`encryptedCredential` was its old name: it was always the
50
+ // value itself), stored encrypted.
51
+ const credential = credentialValue ?? legacyCredential;
52
+ if (credential && !(0, encryption_1.isEncryptionKeySet)())
53
+ return res.status(500).json({ message: 'Encryption key is not set' });
54
+ const encryptedCredential = credential ? (0, encryption_1.encrypt)(String(credential)) : undefined;
48
55
  if (!name || !target)
49
56
  return res.status(400).json({ message: 'name and target are required' });
50
57
  const project = await Project.findOne({ _id: projectId });
51
58
  if (!project)
52
59
  return res.status(404).json({ message: 'Project not found' });
53
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
60
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
54
61
  return res.status(403).json({ message: 'Unauthorized' });
55
62
  const connection = {
56
63
  id: crypto_1.default.randomUUID(),
@@ -76,7 +83,7 @@ function distributionRouter(_config) {
76
83
  const project = await Project.findOne({ _id: projectId });
77
84
  if (!project)
78
85
  return res.status(404).json({ message: 'Project not found' });
79
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
86
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
80
87
  return res.status(403).json({ message: 'Unauthorized' });
81
88
  await Project.update({ _id: projectId }, { $pull: { distributionConnections: { id: connectionId } } });
82
89
  res.json({ message: 'Connection deleted' });
@@ -96,7 +103,7 @@ function distributionRouter(_config) {
96
103
  const project = await Project.findOne({ _id: projectId });
97
104
  if (!project)
98
105
  return res.status(404).json({ message: 'Project not found' });
99
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
106
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
100
107
  return res.status(403).json({ message: 'Unauthorized' });
101
108
  const exportDoc = await Export.findOne({ id: exportId, projectId });
102
109
  if (!exportDoc)
@@ -169,7 +176,7 @@ function distributionRouter(_config) {
169
176
  const project = await Project.findOne({ _id: projectId });
170
177
  if (!project)
171
178
  return res.status(404).json({ message: 'Project not found' });
172
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
179
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
173
180
  return res.status(403).json({ message: 'Unauthorized' });
174
181
  const db = getDb();
175
182
  if (!db)
@@ -237,7 +237,7 @@ function exportRouter(config) {
237
237
  const resolvedNodeSchema = nodeSchema ?? DEFAULT_NODE_SCHEMA;
238
238
  const exportType = useConnections ? 'externalApi' : 'json';
239
239
  const project = await Project.findOne({ _id: projectId });
240
- if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
240
+ if (!project || !(0, auth_1.canEditProject)(project, auth.userId)) {
241
241
  return res.status(403).json({ message: 'Unauthorized project access' });
242
242
  }
243
243
  const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
@@ -258,7 +258,7 @@ function exportRouter(config) {
258
258
  const { id: projectId, export_id: exportId } = req.params;
259
259
  const { name, description, isPrivate, allowedOrigin, devMode, devPorts, useConnections, useCredentials, nodeSchema } = req.body;
260
260
  const project = await Project.findOne({ _id: projectId });
261
- if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
261
+ if (!project || !(0, auth_1.canEditProject)(project, auth.userId)) {
262
262
  return res.status(403).json({ message: 'Unauthorized project access' });
263
263
  }
264
264
  const exportMetadata = project.exports?.find((e) => e.id.toString() === exportId);
@@ -284,7 +284,7 @@ function exportRouter(config) {
284
284
  }
285
285
  const { id: projectId, export_id: exportId } = req.params;
286
286
  const project = await Project.findOne({ _id: projectId });
287
- if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
287
+ if (!project || !(0, auth_1.canEditProject)(project, auth.userId)) {
288
288
  return res.status(403).json({ message: 'Unauthorized project access' });
289
289
  }
290
290
  const exportMetadata = project.exports.find((e) => e.id.toString() === exportId);
@@ -352,7 +352,8 @@ function exportRouter(config) {
352
352
  }
353
353
  let data;
354
354
  if (exportMetadata.nodeSchema) {
355
- data = await (0, executor_1.executeExport)(exportMetadata.nodeSchema, project, config);
355
+ // Read without a session: only the nodes that read data run (see executeExport).
356
+ data = await (0, executor_1.executeExport)(exportMetadata.nodeSchema, project, config, { public: true });
356
357
  return res.json(data ?? null);
357
358
  }
358
359
  // Legacy path: raw JSON or external API export
@@ -25,11 +25,24 @@ function mediaRouter(config) {
25
25
  const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
26
26
  const Project = (0, manager_1.getModel)('projects');
27
27
  // GET /jobs/:jobId — poll job status
28
- router.get('/jobs/:jobId', (req, res) => {
29
- const job = (0, jobQueue_1.getJob)(req.params.jobId);
30
- if (!job)
31
- return res.status(404).json({ message: 'Job not found' });
32
- res.json({ job });
28
+ // Only for whoever started it, or a member of its project: someone else's job is as if it
29
+ // didn't exist.
30
+ router.get('/jobs/:jobId', async (req, res) => {
31
+ try {
32
+ const auth = req.auth;
33
+ const job = (0, jobQueue_1.getJob)(req.params.jobId);
34
+ if (!job)
35
+ return res.status(404).json({ message: 'Job not found' });
36
+ if (job.userId !== auth.userId) {
37
+ const project = await Project.findOne({ _id: job.projectId });
38
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
39
+ return res.status(404).json({ message: 'Job not found' });
40
+ }
41
+ res.json({ job });
42
+ }
43
+ catch (err) {
44
+ res.status(500).json({ message: 'Failed to fetch job', details: err.message });
45
+ }
33
46
  });
34
47
  // POST /projects/:projectId/jobs/ingest — queue an ingest job for an already-uploaded file
35
48
  router.post('/projects/:projectId/jobs/ingest', async (req, res) => {
@@ -42,7 +55,7 @@ function mediaRouter(config) {
42
55
  const project = await Project.findOne({ _id: projectId });
43
56
  if (!project)
44
57
  return res.status(404).json({ message: 'Project not found' });
45
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
58
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
46
59
  return res.status(403).json({ message: 'Unauthorized' });
47
60
  const job = (0, jobQueue_1.createJob)('ingest', auth.userId, projectId, { fileId });
48
61
  // Run async — response returns immediately with jobId
@@ -64,7 +77,7 @@ function mediaRouter(config) {
64
77
  const project = await Project.findOne({ _id: projectId });
65
78
  if (!project)
66
79
  return res.status(404).json({ message: 'Project not found' });
67
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
80
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
68
81
  return res.status(403).json({ message: 'Unauthorized' });
69
82
  const job = (0, jobQueue_1.createJob)('transcode', auth.userId, projectId, {
70
83
  fileId, codec, resolution, outputFormat, bitrate, audioCodec,
@@ -87,7 +100,7 @@ function mediaRouter(config) {
87
100
  const project = await Project.findOne({ _id: projectId });
88
101
  if (!project)
89
102
  return res.status(404).json({ message: 'Project not found' });
90
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
103
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
91
104
  return res.status(403).json({ message: 'Unauthorized' });
92
105
  const job = (0, jobQueue_1.createJob)('thumbnail', auth.userId, projectId, { fileId, timecode, resolution, strategy });
93
106
  setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, { timecode, resolution, strategy }));
@@ -108,7 +121,7 @@ function mediaRouter(config) {
108
121
  const project = await Project.findOne({ _id: projectId });
109
122
  if (!project)
110
123
  return res.status(404).json({ message: 'Project not found' });
111
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
124
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
112
125
  return res.status(403).json({ message: 'Unauthorized' });
113
126
  const job = (0, jobQueue_1.createJob)('caption', auth.userId, projectId, { fileId, sourceLanguage, outputFormat, provider });
114
127
  setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, { sourceLanguage, outputFormat, provider }));
@@ -150,7 +163,7 @@ function mediaRouter(config) {
150
163
  const project = await Project.findOne({ _id: projectId });
151
164
  if (!project)
152
165
  return res.status(404).json({ message: 'Project not found' });
153
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
166
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
154
167
  return res.status(403).json({ message: 'Unauthorized' });
155
168
  const col = getMetadataCollection();
156
169
  if (!col)
@@ -52,9 +52,6 @@ function memberRouter(config) {
52
52
  router.post('/projects/:id/members', async (req, res) => {
53
53
  try {
54
54
  const auth = req.auth;
55
- if (auth.role !== 'admin') {
56
- return res.status(403).json({ message: 'Only admins can invite members' });
57
- }
58
55
  const projectId = req.params.id;
59
56
  const { userId, role } = req.body;
60
57
  if (!userId || !role) {
@@ -67,7 +64,7 @@ function memberRouter(config) {
67
64
  if (!project) {
68
65
  return res.status(404).json({ message: 'Project not found' });
69
66
  }
70
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
67
+ if (!(0, auth_1.isProjectAdmin)(project, auth.userId)) {
71
68
  return res.status(403).json({ message: 'Unauthorized project access' });
72
69
  }
73
70
  const alreadyMember = project.members?.some((m) => m.userId?.toString() === userId?.toString());
@@ -107,9 +104,6 @@ function memberRouter(config) {
107
104
  router.patch('/projects/:id/members/:userId', async (req, res) => {
108
105
  try {
109
106
  const auth = req.auth;
110
- if (auth.role !== 'admin') {
111
- return res.status(403).json({ message: 'Only admins can update member roles' });
112
- }
113
107
  const projectId = req.params.id;
114
108
  const targetUserId = req.params.userId;
115
109
  const { role } = req.body;
@@ -120,7 +114,7 @@ function memberRouter(config) {
120
114
  if (!project) {
121
115
  return res.status(404).json({ message: 'Project not found' });
122
116
  }
123
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
117
+ if (!(0, auth_1.isProjectAdmin)(project, auth.userId)) {
124
118
  return res.status(403).json({ message: 'Unauthorized project access' });
125
119
  }
126
120
  const memberExists = project.members?.some((m) => m.userId?.toString() === targetUserId?.toString());
@@ -282,9 +276,6 @@ function memberRouter(config) {
282
276
  router.delete('/projects/:id/members/:userId', async (req, res) => {
283
277
  try {
284
278
  const auth = req.auth;
285
- if (auth.role !== 'admin') {
286
- return res.status(403).json({ message: 'Only admins can remove members' });
287
- }
288
279
  const projectId = req.params.id;
289
280
  const targetUserId = req.params.userId;
290
281
  if (targetUserId === auth.userId) {
@@ -294,7 +285,7 @@ function memberRouter(config) {
294
285
  if (!project) {
295
286
  return res.status(404).json({ message: 'Project not found' });
296
287
  }
297
- if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
288
+ if (!(0, auth_1.isProjectAdmin)(project, auth.userId)) {
298
289
  return res.status(403).json({ message: 'Unauthorized project access' });
299
290
  }
300
291
  const memberExists = project.members?.some((m) => m.userId?.toString() === targetUserId?.toString());
@@ -1,3 +1,3 @@
1
1
  import { Router } from 'express';
2
2
  import { StreamByConfig } from '../../types';
3
- export declare function credentialRouter(config: StreamByConfig): Router;
3
+ export declare function oauthRouter(config: StreamByConfig): Router;