streamby-core 0.33.0 → 0.35.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 (54) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/LICENSE +661 -0
  3. package/README.md +111 -11
  4. package/dist/adapters/database/connectionManager.js +18 -0
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.js +4 -1
  7. package/dist/middleware/createRouter.js +19 -2
  8. package/dist/middleware/routes/ai.js +3 -3
  9. package/dist/middleware/routes/{credential.d.ts → apiToken.d.ts} +1 -1
  10. package/dist/middleware/routes/apiToken.js +58 -0
  11. package/dist/middleware/routes/connection.js +3 -12
  12. package/dist/middleware/routes/dbConnection.js +7 -26
  13. package/dist/middleware/routes/distribution.js +12 -5
  14. package/dist/middleware/routes/export.js +5 -4
  15. package/dist/middleware/routes/media.js +27 -10
  16. package/dist/middleware/routes/member.js +3 -12
  17. package/dist/middleware/routes/pipeline.js +3 -3
  18. package/dist/middleware/routes/production.js +9 -9
  19. package/dist/middleware/routes/project.js +12 -25
  20. package/dist/middleware/routes/renderFarm.js +13 -6
  21. package/dist/middleware/routes/review.js +4 -4
  22. package/dist/middleware/routes/rights.js +1 -1
  23. package/dist/middleware/routes/storage.js +21 -15
  24. package/dist/middleware/routes/storageConnection.js +2 -8
  25. package/dist/middleware/routes/workflow.js +1 -1
  26. package/dist/services/apiTokens.d.ts +47 -0
  27. package/dist/services/apiTokens.js +110 -0
  28. package/dist/services/auth.d.ts +5 -3
  29. package/dist/services/auth.js +49 -11
  30. package/dist/services/connectionResolver.js +2 -2
  31. package/dist/services/executor.d.ts +4 -1
  32. package/dist/services/executor.js +29 -11
  33. package/dist/services/reencryptCredentials.d.ts +5 -0
  34. package/dist/services/reencryptCredentials.js +74 -0
  35. package/dist/services/userIntegration.d.ts +2 -2
  36. package/dist/services/userIntegration.js +7 -1
  37. package/dist/services/wsHub.js +6 -0
  38. package/dist/types/index.d.ts +9 -0
  39. package/dist/utils/auth.d.ts +15 -1
  40. package/dist/utils/auth.js +21 -6
  41. package/dist/utils/encryption.d.ts +6 -1
  42. package/dist/utils/encryption.js +80 -27
  43. package/dist/utils/serviceEndpoints.d.ts +2 -0
  44. package/dist/utils/serviceEndpoints.js +26 -0
  45. package/package.json +4 -3
  46. package/dist/middleware/routes/apiConnection.d.ts +0 -3
  47. package/dist/middleware/routes/apiConnection.js +0 -100
  48. package/dist/middleware/routes/credential.js +0 -80
  49. package/dist/services/apiConnection.d.ts +0 -18
  50. package/dist/services/apiConnection.js +0 -65
  51. package/dist/services/credential.d.ts +0 -8
  52. package/dist/services/credential.js +0 -70
  53. package/dist/services/pipeline.d.ts +0 -2
  54. package/dist/services/pipeline.js +0 -292
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # @streamby/core
1
+ # streamby-core
2
+
3
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
2
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.
@@ -7,7 +9,7 @@ Part of the [StreamBy](https://streamby.nhexa.cl) ecosystem by TerminalCore Labs
7
9
 
8
10
  ## 🚀 What is it?
9
11
 
10
- `@streamby/core` is a plug-and-play middleware for Express (or compatible frameworks) that enables file uploads, listings and **multi-project access** over services like:
12
+ `streamby-core` is a plug-and-play middleware for Express (or compatible frameworks) that enables file uploads, listings and **multi-project access** over services like:
11
13
 
12
14
  - ✅ AWS S3
13
15
  - ✅ Google Cloud Storage (soon)
@@ -21,7 +23,7 @@ It's designed to be installed inside your existing API as a library — **no nee
21
23
  ## 📦 Installation
22
24
 
23
25
  ```bash
24
- npm install @streamby/core ws
26
+ npm install streamby-core ws
25
27
  npm install -D @types/ws
26
28
  ```
27
29
 
@@ -33,7 +35,7 @@ npm install -D @types/ws
33
35
  import http from 'http';
34
36
  import express from 'express';
35
37
  import { WebSocketServer } from 'ws';
36
- import { createStreamByRouter } from '@streamby/core';
38
+ import { createStreamByRouter } from 'streamby-core';
37
39
 
38
40
  const app = express();
39
41
  const server = http.createServer(app);
@@ -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,9 +73,103 @@ 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
+ ## 🎫 API tokens
104
+
105
+ Agents and services work with StreamBy as a user through API tokens, without the user's
106
+ browser session. A user manages their own tokens (`GET`, `POST` and
107
+ `DELETE /streamby/user/api-tokens`, only with a session). Each token has:
108
+
109
+ - **an access level**: `read` (only reads) or `write` (reads and writes);
110
+ - **the projects it reaches**: a list of project ids, or `'*'` for every project the user is a
111
+ member of, including ones they join later;
112
+ - **an expiry**: 7, 30, 90 or 365 days, or none.
113
+
114
+ A token is `sbt_` followed by 32 random bytes. It's shown once, when it's made. Only its
115
+ SHA-256 hash is stored, with a short prefix to tell it apart.
116
+
117
+ Requests send it as `Authorization: Bearer sbt_...`. Your `authProvider` resolves it with
118
+ `resolveApiToken` and returns its owner, with the token's scope in `apiToken`:
119
+
120
+ ```ts
121
+ import { createStreamByRouter, resolveApiToken, API_TOKEN_PREFIX } from 'streamby-core';
122
+
123
+ createStreamByRouter({
124
+ // ...
125
+ authProvider: async (req) => {
126
+ const bearer = req.headers.authorization?.replace(/^Bearer /, '');
127
+ if (bearer?.startsWith(API_TOKEN_PREFIX)) {
128
+ const resolved = await resolveApiToken(bearer); // null once revoked or expired
129
+ if (!resolved) throw new Error('Invalid API token');
130
+ const user = await findUser(resolved.userId); // your own user lookup
131
+ return { userId: user.id, username: user.username, role: user.role, apiToken: resolved.scope };
132
+ }
133
+ // ...your session-based authentication
134
+ },
135
+ });
136
+ ```
137
+
138
+ A request made with a token reaches only what the token's scope allows:
139
+
140
+ - a `read` token can't write;
141
+ - a project outside the token's projects answers 404, and the project list shows only the
142
+ token's projects;
143
+ - a token can't manage tokens;
144
+ - outside a project, a token can only make a few reads (`/auth`, `/projects`, `/jobs/:jobId`,
145
+ `/user/subscription`, `/users/search`).
146
+
147
+ The owner's role in each project still applies on top of that. A token never does more than
148
+ its owner can, and it stops reaching a project as soon as its owner leaves it.
149
+
150
+ ---
151
+
152
+ ## 👥 Project roles
153
+
154
+ What a user can do in a project depends on their role in that project, as an active member
155
+ (an invitation counts once it's accepted). It never depends on the platform role your
156
+ `authProvider` returns.
157
+
158
+ | Role | Can |
159
+ |---|---|
160
+ | `viewer` | See the project and its content, and archive the project in their own list. |
161
+ | `editor` | Everything a viewer can, plus change the project: its details, connections, exports, files and folders, workflow, pipelines, production, reviews, rights, and start jobs. |
162
+ | `admin` | Everything an editor can, plus manage the project's members and delete the project. |
163
+
164
+ A project's public exports (`GET /streamby/:projectId/export/:exportName`) are read without a
165
+ session. They only run nodes that read data: never a node that uses an account's credential,
166
+ nor one that starts a job.
167
+
168
+ ---
169
+
74
170
  ## 🗄️ Database Schema Setup
75
171
 
76
- `@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.
172
+ `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.
77
173
 
78
174
  The library will only interact with the specified schema and will not touch other schemas in your database. The `reset` option is opt-in and will only drop and recreate the specified `streamby` schema if explicitly set to `true`.
79
175
 
@@ -81,7 +177,7 @@ The library will only interact with the specified schema and will not touch othe
81
177
 
82
178
  ```ts
83
179
  import { Pool } from "pg";
84
- import { setupStreambyPg } from "@streamby/core/pg/setup"; // Adjust path as per your package structure
180
+ import { setupStreambyPg } from "streamby-core/pg/setup"; // Adjust path as per your package structure
85
181
 
86
182
  const pool = new Pool({ connectionString: process.env.DATABASE_URL });
87
183
 
@@ -94,7 +190,7 @@ await setupStreambyPg({ pool, schema: "streamby", reset: false });
94
190
 
95
191
  ```ts
96
192
  import { MongoClient } from "mongodb";
97
- import { setupStreambyMongo } from "@streamby/core/mongo/setup"; // Adjust path as per your package structure
193
+ import { setupStreambyMongo } from "streamby-core/mongo/setup"; // Adjust path as per your package structure
98
194
 
99
195
  const client = new MongoClient(process.env.MONGO_URL!);
100
196
 
@@ -107,7 +203,7 @@ await setupStreambyMongo({ client, dbName: "streamby" });
107
203
 
108
204
  ```ts
109
205
  import express from 'express';
110
- import { createStreamByRouter } from '@streamby/core';
206
+ import { createStreamByRouter } from 'streamby-core';
111
207
 
112
208
  const app = express();
113
209
 
@@ -168,7 +264,7 @@ Unit tests use [Vitest](https://vitest.dev) and [Supertest](https://www.npmjs.co
168
264
 
169
265
  ## 🗺️ API Endpoints
170
266
 
171
- Here's a summary of the key API endpoints provided by `@streamby/core`:
267
+ Here's a summary of the key API endpoints provided by `streamby-core`:
172
268
 
173
269
  ### `GET /streamby/projects`
174
270
 
@@ -279,5 +375,9 @@ Visit us [here](https://terminalcore.cl)
279
375
 
280
376
  ## 📟 License
281
377
 
282
- [MIT](./LICENSE)
378
+ Copyright (C) 2026 TerminalCore Labs
379
+
380
+ Licensed under the [GNU Affero General Public License v3.0](./LICENSE) (AGPL-3.0-only).
381
+
382
+ > **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
383
 
@@ -147,6 +147,24 @@ const ensureTablesExist = async (pool) => {
147
147
  CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
148
148
  `);
149
149
  console.log('✅ "user_integrations" table ensured to exist.');
150
+ // API tokens: only their hash is kept (see services/apiTokens). Main db only, like
151
+ // user_integrations above.
152
+ await pool.query(`
153
+ CREATE TABLE IF NOT EXISTS streamby.api_tokens (
154
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
155
+ "userId" VARCHAR(255) NOT NULL,
156
+ name VARCHAR(255) NOT NULL,
157
+ prefix VARCHAR(32) NOT NULL,
158
+ hash CHAR(64) NOT NULL UNIQUE,
159
+ access VARCHAR(10) NOT NULL,
160
+ "projectIds" JSONB NOT NULL,
161
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
162
+ "expiresAt" TIMESTAMP WITH TIME ZONE,
163
+ "lastUsedAt" TIMESTAMP WITH TIME ZONE
164
+ );
165
+ CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
166
+ `);
167
+ console.log('✅ "api_tokens" table ensured to exist.');
150
168
  }
151
169
  catch (error) {
152
170
  console.error('❌ Error ensuring tables exist:', error);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { createStreamByRouter } from './middleware/createRouter';
2
2
  export { emitToUser } from './services/wsHub';
3
3
  export { createNotification } from './services/notification';
4
- export type { UserPlan, UserSubscription } from './types';
4
+ export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenAccess } from './types';
5
+ export { resolveApiToken, API_TOKEN_PREFIX } from './services/apiTokens';
6
+ export type { ResolvedApiToken } from './services/apiTokens';
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
3
+ 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");
7
7
  Object.defineProperty(exports, "emitToUser", { enumerable: true, get: function () { return wsHub_1.emitToUser; } });
8
8
  var notification_1 = require("./services/notification");
9
9
  Object.defineProperty(exports, "createNotification", { enumerable: true, get: function () { return notification_1.createNotification; } });
10
+ var apiTokens_1 = require("./services/apiTokens");
11
+ Object.defineProperty(exports, "resolveApiToken", { enumerable: true, get: function () { return apiTokens_1.resolveApiToken; } });
12
+ Object.defineProperty(exports, "API_TOKEN_PREFIX", { enumerable: true, get: function () { return apiTokens_1.API_TOKEN_PREFIX; } });
@@ -9,8 +9,10 @@ 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");
15
+ const apiToken_1 = require("./routes/apiToken");
14
16
  const encryption_1 = require("../utils/encryption");
15
17
  const auth_2 = require("./routes/auth");
16
18
  const user_1 = require("./routes/user");
@@ -38,7 +40,7 @@ const pipeline_1 = require("./routes/pipeline");
38
40
  function createStreamByRouter(config) {
39
41
  const router = express_1.default.Router();
40
42
  if (config.encrypt)
41
- (0, encryption_1.setEncryptionKey)(config.encrypt);
43
+ (0, encryption_1.setEncryptionKey)(config.encrypt, config.encryptPreviousKeys);
42
44
  // Establish DB connections asynchronously (Postgres/Mongo + schema migrations).
43
45
  // Until they're ready, HOLD incoming requests instead of letting them through —
44
46
  // route handlers resolve projects/exports (e.g. the /menu-list export) against a
@@ -52,6 +54,16 @@ function createStreamByRouter(config) {
52
54
  .then(() => (0, backfillBuiltinConnections_1.backfillBuiltinConnections)(config))
53
55
  .catch((err) => {
54
56
  console.error('❌ StreamBy: failed to backfill built-in connections:', err);
57
+ })
58
+ .then(() => (0, reencryptCredentials_1.reencryptStoredCredentials)())
59
+ .then(({ updated, failed }) => {
60
+ if (updated)
61
+ console.log(`✅ StreamBy: re-encrypted the credentials of ${updated} record(s).`);
62
+ if (failed)
63
+ console.error(`❌ StreamBy: ${failed} stored credential(s) could not be decrypted with the configured keys; left as they are.`);
64
+ })
65
+ .catch((err) => {
66
+ console.error('❌ StreamBy: failed to re-encrypt stored credentials:', err);
55
67
  })
56
68
  .finally(() => {
57
69
  ready = true;
@@ -84,15 +96,20 @@ function createStreamByRouter(config) {
84
96
  (0, manager_1.registerModel)('users', [mainDb.id], 'users', 'accounts');
85
97
  (0, manager_1.registerModel)('user_subscriptions', [mainDb.id], 'user_subscriptions', 'streamby');
86
98
  (0, manager_1.registerModel)('user_integrations', [mainDb.id], 'user_integrations', streambySchema);
99
+ (0, manager_1.registerModel)('api_tokens', [mainDb.id], 'api_tokens', streambySchema);
87
100
  }
88
101
  }
89
102
  if (config.websocket?.server) {
90
103
  (0, wsHub_1.initWsHub)(config.websocket.server, config);
91
104
  }
92
- router.use((0, user_1.userRouter)(config));
105
+ // /auth answers whether there's a session (it reads it itself); everything after needs one,
106
+ // but for the public export route (see authenticate).
93
107
  router.use((0, auth_2.authRouter)(config));
94
108
  router.use((0, auth_1.authenticate)(config));
109
+ router.use(auth_1.enforceApiTokenScope);
95
110
  router.use(subscription_1.ensureSubscription);
111
+ router.use((0, user_1.userRouter)(config));
112
+ router.use((0, apiToken_1.apiTokenRouter)(config));
96
113
  router.use((0, export_1.exportRouter)(config));
97
114
  router.use((0, member_1.memberRouter)(config));
98
115
  router.use((0, storage_2.storageRouter)(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 }));
@@ -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 apiTokenRouter(config: StreamByConfig): Router;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiTokenRouter = apiTokenRouter;
4
+ const express_1 = require("express");
5
+ const manager_1 = require("../../models/manager");
6
+ const auth_1 = require("../../utils/auth");
7
+ const apiTokens_1 = require("../../services/apiTokens");
8
+ // The signed-in user's own API tokens (see services/apiTokens): listed, made, revoked. Only
9
+ // with a session: a token can't manage tokens (see enforceApiTokenScope).
10
+ function apiTokenRouter(config) {
11
+ const router = (0, express_1.Router)();
12
+ const Project = (0, manager_1.getModel)('projects');
13
+ router.get('/user/api-tokens', async (req, res) => {
14
+ try {
15
+ const auth = req.auth;
16
+ res.json({ apiTokens: await (0, apiTokens_1.listApiTokens)(auth.userId) });
17
+ }
18
+ catch (err) {
19
+ res.status(500).json({ message: 'Failed to list API tokens', details: err.message });
20
+ }
21
+ });
22
+ // The token itself is in this response only: it's never stored, nor shown again.
23
+ router.post('/user/api-tokens', async (req, res) => {
24
+ try {
25
+ const auth = req.auth;
26
+ const { name, access, projectIds, expiresInDays } = req.body ?? {};
27
+ // A token reaches only projects its owner is in.
28
+ if (Array.isArray(projectIds)) {
29
+ for (const id of projectIds) {
30
+ const project = typeof id === 'string' ? await Project.findOne({ _id: id }) : null;
31
+ if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
32
+ return res.status(400).json({ message: `You are not a member of project '${id}'` });
33
+ }
34
+ }
35
+ }
36
+ const { token, info } = await (0, apiTokens_1.createApiToken)(auth.userId, { name, access, projectIds, expiresInDays: expiresInDays ?? null }, config);
37
+ res.status(201).json({ token, apiToken: info, message: 'Copy the token now: it will not be shown again' });
38
+ }
39
+ catch (err) {
40
+ if (err instanceof apiTokens_1.ApiTokenError)
41
+ return res.status(err.status).json({ message: err.message });
42
+ res.status(500).json({ message: 'Failed to create API token', details: err.message });
43
+ }
44
+ });
45
+ router.delete('/user/api-tokens/:tokenId', async (req, res) => {
46
+ try {
47
+ const auth = req.auth;
48
+ const revoked = await (0, apiTokens_1.revokeApiToken)(auth.userId, req.params.tokenId);
49
+ if (!revoked)
50
+ return res.status(404).json({ message: 'API token not found' });
51
+ res.status(204).end();
52
+ }
53
+ catch (err) {
54
+ res.status(500).json({ message: 'Failed to revoke API token', details: err.message });
55
+ }
56
+ });
57
+ return router;
58
+ }
@@ -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