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.
- package/CHANGELOG.md +100 -0
- package/LICENSE +661 -0
- package/README.md +111 -11
- package/dist/adapters/database/connectionManager.js +18 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/middleware/createRouter.js +19 -2
- package/dist/middleware/routes/ai.js +3 -3
- package/dist/middleware/routes/{credential.d.ts → apiToken.d.ts} +1 -1
- package/dist/middleware/routes/apiToken.js +58 -0
- package/dist/middleware/routes/connection.js +3 -12
- package/dist/middleware/routes/dbConnection.js +7 -26
- package/dist/middleware/routes/distribution.js +12 -5
- package/dist/middleware/routes/export.js +5 -4
- package/dist/middleware/routes/media.js +27 -10
- package/dist/middleware/routes/member.js +3 -12
- package/dist/middleware/routes/pipeline.js +3 -3
- package/dist/middleware/routes/production.js +9 -9
- package/dist/middleware/routes/project.js +12 -25
- package/dist/middleware/routes/renderFarm.js +13 -6
- package/dist/middleware/routes/review.js +4 -4
- package/dist/middleware/routes/rights.js +1 -1
- package/dist/middleware/routes/storage.js +21 -15
- package/dist/middleware/routes/storageConnection.js +2 -8
- package/dist/middleware/routes/workflow.js +1 -1
- package/dist/services/apiTokens.d.ts +47 -0
- package/dist/services/apiTokens.js +110 -0
- package/dist/services/auth.d.ts +5 -3
- package/dist/services/auth.js +49 -11
- package/dist/services/connectionResolver.js +2 -2
- package/dist/services/executor.d.ts +4 -1
- package/dist/services/executor.js +29 -11
- package/dist/services/reencryptCredentials.d.ts +5 -0
- package/dist/services/reencryptCredentials.js +74 -0
- package/dist/services/userIntegration.d.ts +2 -2
- package/dist/services/userIntegration.js +7 -1
- package/dist/services/wsHub.js +6 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/auth.d.ts +15 -1
- package/dist/utils/auth.js +21 -6
- package/dist/utils/encryption.d.ts +6 -1
- package/dist/utils/encryption.js +80 -27
- package/dist/utils/serviceEndpoints.d.ts +2 -0
- package/dist/utils/serviceEndpoints.js +26 -0
- package/package.json +4 -3
- package/dist/middleware/routes/apiConnection.d.ts +0 -3
- package/dist/middleware/routes/apiConnection.js +0 -100
- package/dist/middleware/routes/credential.js +0 -80
- package/dist/services/apiConnection.d.ts +0 -18
- package/dist/services/apiConnection.js +0 -65
- package/dist/services/credential.d.ts +0 -8
- package/dist/services/credential.js +0 -70
- package/dist/services/pipeline.d.ts +0 -2
- package/dist/services/pipeline.js +0 -292
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# streamby-core
|
|
2
|
+
|
|
3
|
+
[](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
|
-
|
|
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
|
|
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 '
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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 '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 }));
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|