streamby-core 0.29.8 → 0.29.9
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/dist/adapters/database/connectionManager.js +26 -7
- package/dist/middleware/createRouter.js +8 -0
- package/dist/middleware/routes/dbConnection.js +30 -11
- package/dist/middleware/routes/project.js +66 -2
- package/dist/middleware/routes/storage.js +44 -32
- package/dist/middleware/routes/storageConnection.js +11 -17
- package/dist/middleware/routes/userIntegration.d.ts +3 -0
- package/dist/middleware/routes/userIntegration.js +93 -0
- package/dist/services/backfillBuiltinConnections.d.ts +2 -0
- package/dist/services/backfillBuiltinConnections.js +56 -0
- package/dist/services/userIntegration.d.ts +16 -0
- package/dist/services/userIntegration.js +73 -0
- package/dist/types/index.d.ts +22 -4
- package/dist/utils/builtinAccess.d.ts +4 -0
- package/dist/utils/builtinAccess.js +19 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +7 -0
- package/package.json +1 -1
|
@@ -73,13 +73,14 @@ const ensureTablesExist = async (pool) => {
|
|
|
73
73
|
console.log('✅ "projects" table ensured to exist.');
|
|
74
74
|
// Add JSONB columns to existing tables that predate this schema version
|
|
75
75
|
await pool.query(`
|
|
76
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin"
|
|
77
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports
|
|
78
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials
|
|
79
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections"
|
|
80
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections"
|
|
81
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS
|
|
82
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS
|
|
76
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
|
|
77
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
|
|
78
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials JSONB DEFAULT '[]';
|
|
79
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections" JSONB DEFAULT '[]';
|
|
80
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
|
|
81
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
|
|
82
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
|
|
83
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
|
|
83
84
|
`);
|
|
84
85
|
// Ensure dbType exists with a non-null default
|
|
85
86
|
await pool.query(`
|
|
@@ -124,6 +125,24 @@ const ensureTablesExist = async (pool) => {
|
|
|
124
125
|
);
|
|
125
126
|
`);
|
|
126
127
|
console.log('✅ "exports" table ensured to exist.');
|
|
128
|
+
// user_integrations is registered against the main db only (see createRouter.ts), but
|
|
129
|
+
// this function runs per-SQL-connection like projects/exports above — harmless on
|
|
130
|
+
// non-main connections since the model never queries them there.
|
|
131
|
+
await pool.query(`
|
|
132
|
+
CREATE TABLE IF NOT EXISTS streamby.user_integrations (
|
|
133
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
134
|
+
"userId" VARCHAR(255) NOT NULL,
|
|
135
|
+
kind VARCHAR(20) NOT NULL,
|
|
136
|
+
provider VARCHAR(20) NOT NULL,
|
|
137
|
+
name VARCHAR(255) NOT NULL,
|
|
138
|
+
description TEXT,
|
|
139
|
+
"encryptedCredential" TEXT NOT NULL,
|
|
140
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
141
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
142
|
+
);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
|
|
144
|
+
`);
|
|
145
|
+
console.log('✅ "user_integrations" table ensured to exist.');
|
|
127
146
|
}
|
|
128
147
|
catch (error) {
|
|
129
148
|
console.error('❌ Error ensuring tables exist:', error);
|
|
@@ -8,6 +8,7 @@ const express_1 = __importDefault(require("express"));
|
|
|
8
8
|
const manager_1 = require("../models/manager");
|
|
9
9
|
const storage_1 = require("../providers/storage");
|
|
10
10
|
const connectionManager_1 = require("../adapters/database/connectionManager");
|
|
11
|
+
const backfillBuiltinConnections_1 = require("../services/backfillBuiltinConnections");
|
|
11
12
|
const wsHub_1 = require("../services/wsHub");
|
|
12
13
|
const auth_1 = require("../services/auth");
|
|
13
14
|
const encryption_1 = require("../utils/encryption");
|
|
@@ -22,6 +23,7 @@ const credential_1 = require("./routes/credential");
|
|
|
22
23
|
const connection_1 = require("./routes/connection");
|
|
23
24
|
const dbConnection_1 = require("./routes/dbConnection");
|
|
24
25
|
const storageConnection_1 = require("./routes/storageConnection");
|
|
26
|
+
const userIntegration_1 = require("./routes/userIntegration");
|
|
25
27
|
const notification_1 = require("./routes/notification");
|
|
26
28
|
const subscription_1 = require("./routes/subscription");
|
|
27
29
|
const media_1 = require("./routes/media");
|
|
@@ -46,6 +48,10 @@ function createStreamByRouter(config) {
|
|
|
46
48
|
const readyPromise = (0, connectionManager_1.initConnections)(config.databases || [])
|
|
47
49
|
.catch((err) => {
|
|
48
50
|
console.error('❌ StreamBy: failed to initialize database connections:', err);
|
|
51
|
+
})
|
|
52
|
+
.then(() => (0, backfillBuiltinConnections_1.backfillBuiltinConnections)(config))
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
console.error('❌ StreamBy: failed to backfill built-in connections:', err);
|
|
49
55
|
})
|
|
50
56
|
.finally(() => {
|
|
51
57
|
ready = true;
|
|
@@ -77,6 +83,7 @@ function createStreamByRouter(config) {
|
|
|
77
83
|
if (mainDb) {
|
|
78
84
|
(0, manager_1.registerModel)('users', [mainDb.id], 'users', 'accounts');
|
|
79
85
|
(0, manager_1.registerModel)('user_subscriptions', [mainDb.id], 'user_subscriptions', 'streamby');
|
|
86
|
+
(0, manager_1.registerModel)('user_integrations', [mainDb.id], 'user_integrations', streambySchema);
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
if (config.websocket?.server) {
|
|
@@ -95,6 +102,7 @@ function createStreamByRouter(config) {
|
|
|
95
102
|
router.use((0, connection_1.connectionRouter)(config));
|
|
96
103
|
router.use((0, dbConnection_1.dbConnectionRouter)(config));
|
|
97
104
|
router.use((0, storageConnection_1.storageConnectionRouter)(config));
|
|
105
|
+
router.use((0, userIntegration_1.userIntegrationRouter)(config));
|
|
98
106
|
router.use((0, notification_1.notificationRouter)(config));
|
|
99
107
|
router.use((0, subscription_1.subscriptionRouter)(config));
|
|
100
108
|
router.use((0, media_1.mediaRouter)(config));
|
|
@@ -39,11 +39,9 @@ const mongodb_1 = require("mongodb");
|
|
|
39
39
|
const manager_1 = require("../../models/manager");
|
|
40
40
|
const auth_1 = require("../../utils/auth");
|
|
41
41
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
42
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
42
43
|
const dbConnection_1 = require("../../services/dbConnection");
|
|
43
44
|
const VALID_DB_TYPES = ['postgresql', 'mongodb'];
|
|
44
|
-
function isBuiltinDb(connId, config) {
|
|
45
|
-
return (config.databases ?? []).some(db => db.id === connId);
|
|
46
|
-
}
|
|
47
45
|
function resolveBuiltinConnection(connId, config) {
|
|
48
46
|
const db = (config.databases ?? []).find(d => d.id === connId);
|
|
49
47
|
if (!db)
|
|
@@ -127,7 +125,7 @@ function dbConnectionRouter(config) {
|
|
|
127
125
|
// ─── Delete DB connection ─────────────────────────────────────────────────
|
|
128
126
|
router.delete('/projects/:id/connections/db/:connId', async (req, res) => {
|
|
129
127
|
try {
|
|
130
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
128
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
131
129
|
return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
|
|
132
130
|
}
|
|
133
131
|
const auth = req.auth;
|
|
@@ -158,7 +156,10 @@ function dbConnectionRouter(config) {
|
|
|
158
156
|
return res.status(404).json({ message: 'Project not found' });
|
|
159
157
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
160
158
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
161
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
159
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
160
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
161
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
162
|
+
}
|
|
162
163
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
163
164
|
if ('error' in internal)
|
|
164
165
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -190,7 +191,10 @@ function dbConnectionRouter(config) {
|
|
|
190
191
|
const schema = req.body;
|
|
191
192
|
if (!schema.tableName)
|
|
192
193
|
return res.status(400).json({ message: 'tableName is required' });
|
|
193
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
194
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
195
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
196
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
197
|
+
}
|
|
194
198
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
195
199
|
if ('error' in internal)
|
|
196
200
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -225,7 +229,10 @@ function dbConnectionRouter(config) {
|
|
|
225
229
|
return res.status(404).json({ message: 'Project not found' });
|
|
226
230
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
227
231
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
228
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
232
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
233
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
234
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
235
|
+
}
|
|
229
236
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
230
237
|
if ('error' in internal)
|
|
231
238
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -253,7 +260,10 @@ function dbConnectionRouter(config) {
|
|
|
253
260
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
254
261
|
const limit = Math.min(parseInt(String(req.query.limit ?? 50), 10), 500);
|
|
255
262
|
const offset = Math.max(parseInt(String(req.query.offset ?? 0), 10), 0);
|
|
256
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
263
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
264
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
265
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
266
|
+
}
|
|
257
267
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
258
268
|
if ('error' in internal)
|
|
259
269
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -286,7 +296,10 @@ function dbConnectionRouter(config) {
|
|
|
286
296
|
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
287
297
|
return res.status(400).json({ message: 'Request body must be a JSON object' });
|
|
288
298
|
}
|
|
289
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
299
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
300
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
301
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
302
|
+
}
|
|
290
303
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
291
304
|
if ('error' in internal)
|
|
292
305
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -318,7 +331,10 @@ function dbConnectionRouter(config) {
|
|
|
318
331
|
if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
|
|
319
332
|
return res.status(400).json({ message: 'Request body must be a JSON object' });
|
|
320
333
|
}
|
|
321
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
334
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
335
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
336
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
337
|
+
}
|
|
322
338
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
323
339
|
if ('error' in internal)
|
|
324
340
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -346,7 +362,10 @@ function dbConnectionRouter(config) {
|
|
|
346
362
|
return res.status(404).json({ message: 'Project not found' });
|
|
347
363
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
348
364
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
349
|
-
if (isBuiltinDb(req.params.connId, config)) {
|
|
365
|
+
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
366
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
|
|
367
|
+
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
368
|
+
}
|
|
350
369
|
const internal = resolveBuiltinConnection(req.params.connId, config);
|
|
351
370
|
if ('error' in internal)
|
|
352
371
|
return res.status(internal.status).json({ message: internal.error });
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.projectRouter = projectRouter;
|
|
4
7
|
const express_1 = require("express");
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
5
9
|
const manager_1 = require("../../models/manager");
|
|
6
10
|
const auth_1 = require("../../utils/auth");
|
|
7
11
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
8
12
|
const sql_1 = require("../../adapters/database/sql");
|
|
9
13
|
const sanitize_1 = require("../../utils/sanitize");
|
|
14
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
10
15
|
function projectRouter(config) {
|
|
11
16
|
const router = (0, express_1.Router)();
|
|
12
17
|
const Project = (0, manager_1.getModel)('projects');
|
|
@@ -74,7 +79,7 @@ function projectRouter(config) {
|
|
|
74
79
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
75
80
|
return res.status(403).json({ message: 'Permission denied' });
|
|
76
81
|
}
|
|
77
|
-
const { name, description, image, allowedOrigin, public: isPublic = true, category } = req.body;
|
|
82
|
+
const { name, description, image, allowedOrigin, public: isPublic = true, category, integrationIds } = req.body;
|
|
78
83
|
if (isPublic === false && req.subscription === 'freemium') {
|
|
79
84
|
return res.status(403).json({ error: 'Private projects require a subscriber plan' });
|
|
80
85
|
}
|
|
@@ -88,7 +93,55 @@ function projectRouter(config) {
|
|
|
88
93
|
if (!user) {
|
|
89
94
|
return res.status(404).json({ message: 'User not found' });
|
|
90
95
|
}
|
|
91
|
-
|
|
96
|
+
// Classify + revalidate every integrationId server-side — never trust a client-supplied
|
|
97
|
+
// builtin id. Any invalid id rejects the whole request (no project is created).
|
|
98
|
+
const dbConnections = [];
|
|
99
|
+
const storageConnections = [];
|
|
100
|
+
if (Array.isArray(integrationIds)) {
|
|
101
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
102
|
+
for (const id of integrationIds) {
|
|
103
|
+
const now = new Date();
|
|
104
|
+
if ((0, builtinAccess_1.isBuiltinDb)(id, config)) {
|
|
105
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'database'))) {
|
|
106
|
+
return res.status(403).json({ message: `Access to built-in database '${id}' is not permitted` });
|
|
107
|
+
}
|
|
108
|
+
const db = config.databases.find(d => d.id === id);
|
|
109
|
+
dbConnections.push({
|
|
110
|
+
id: crypto_1.default.randomUUID(), name: id, dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
|
|
111
|
+
credentialId: '', projectId: '', createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if ((0, builtinAccess_1.isBuiltinStorageId)(id, config)) {
|
|
115
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'storage'))) {
|
|
116
|
+
return res.status(403).json({ message: `Access to built-in storage '${id}' is not permitted` });
|
|
117
|
+
}
|
|
118
|
+
const provider = config.storageProviders.find(p => p.id === id);
|
|
119
|
+
storageConnections.push({
|
|
120
|
+
id: crypto_1.default.randomUUID(), name: id, type: provider.type,
|
|
121
|
+
credentialId: '', projectId: '', createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const integration = await UserIntegrationModel.findOne({ id, userId: auth.userId });
|
|
126
|
+
if (!integration) {
|
|
127
|
+
return res.status(403).json({ message: `Integration '${id}' is not valid or does not belong to you` });
|
|
128
|
+
}
|
|
129
|
+
if (integration.kind === 'database') {
|
|
130
|
+
dbConnections.push({
|
|
131
|
+
id: crypto_1.default.randomUUID(), name: integration.name, dbType: integration.provider,
|
|
132
|
+
credentialId: '', projectId: '', createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
storageConnections.push({
|
|
137
|
+
id: crypto_1.default.randomUUID(), name: integration.name, type: integration.provider,
|
|
138
|
+
credentialId: '', projectId: '', createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let newProject = await Project.create({
|
|
92
145
|
dbType,
|
|
93
146
|
name,
|
|
94
147
|
description: description || '',
|
|
@@ -97,7 +150,18 @@ function projectRouter(config) {
|
|
|
97
150
|
public: isPublic,
|
|
98
151
|
category: category || null,
|
|
99
152
|
members: [{ userId: auth.userId, username: user.username, role: "admin", status: "active", archived: false }],
|
|
153
|
+
dbConnections: [],
|
|
154
|
+
storageConnections: [],
|
|
100
155
|
});
|
|
156
|
+
const newProjectId = newProject._id || newProject.id;
|
|
157
|
+
if (dbConnections.length || storageConnections.length) {
|
|
158
|
+
const stamped = await Project.update({ _id: newProjectId }, {
|
|
159
|
+
dbConnections: dbConnections.map(c => ({ ...c, projectId: newProjectId })),
|
|
160
|
+
storageConnections: storageConnections.map(c => ({ ...c, projectId: newProjectId })),
|
|
161
|
+
});
|
|
162
|
+
if (stamped)
|
|
163
|
+
newProject = stamped;
|
|
164
|
+
}
|
|
101
165
|
const allProjects = await Project.find({});
|
|
102
166
|
const projects = allProjects
|
|
103
167
|
.filter(project => {
|
|
@@ -13,6 +13,7 @@ const manager_1 = require("../../models/manager");
|
|
|
13
13
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
14
14
|
const auth_1 = require("../../utils/auth");
|
|
15
15
|
const encryption_1 = require("../../utils/encryption");
|
|
16
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
16
17
|
const mongodb_1 = require("mongodb");
|
|
17
18
|
const crypto_1 = __importDefault(require("crypto"));
|
|
18
19
|
const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
@@ -46,36 +47,43 @@ function getRawFoldersCollection() {
|
|
|
46
47
|
return null;
|
|
47
48
|
return (0, connectionManager_1.getConnection)(activeId).client.db().collection('storage_folders');
|
|
48
49
|
}
|
|
49
|
-
function connFilter(projectId, connId, extra) {
|
|
50
|
-
const isBuiltin = connId === 'builtin' || connId.startsWith('builtin-');
|
|
51
|
-
const connMatch = isBuiltin
|
|
52
|
-
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
|
|
53
|
-
: { storageConnectionId: connId };
|
|
54
|
-
return { projectId, ...connMatch, ...(extra ?? {}) };
|
|
55
|
-
}
|
|
56
|
-
async function resolveConnAdapter(connId, project, globalAdapter) {
|
|
57
|
-
if (connId === 'builtin' || connId.startsWith('builtin-'))
|
|
58
|
-
return globalAdapter;
|
|
59
|
-
const conn = project.storageConnections?.find((c) => c.id === connId);
|
|
60
|
-
if (!conn)
|
|
61
|
-
return { error: 'Storage connection not found', status: 404 };
|
|
62
|
-
if (!(0, encryption_1.isEncryptionKeySet)())
|
|
63
|
-
return { error: 'Encryption key not set', status: 500 };
|
|
64
|
-
const credential = project.credentials?.find((c) => c.id === conn.credentialId);
|
|
65
|
-
if (!credential)
|
|
66
|
-
return { error: 'Credential not found in project', status: 400 };
|
|
67
|
-
try {
|
|
68
|
-
const s3Config = JSON.parse((0, encryption_1.decrypt)(credential.encryptedValue));
|
|
69
|
-
return new s3_1.S3Adapter(s3Config);
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
50
|
function storageRouter(config) {
|
|
76
51
|
const router = (0, express_1.Router)();
|
|
77
52
|
const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
|
|
78
53
|
const Project = (0, manager_1.getModel)('projects');
|
|
54
|
+
function connFilter(projectId, connId, extra) {
|
|
55
|
+
const isBuiltin = (0, builtinAccess_1.isBuiltinStorageId)(connId, config);
|
|
56
|
+
const connMatch = isBuiltin
|
|
57
|
+
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
|
|
58
|
+
: { storageConnectionId: connId };
|
|
59
|
+
return { projectId, ...connMatch, ...(extra ?? {}) };
|
|
60
|
+
}
|
|
61
|
+
async function resolveConnAdapter(connId, project, auth) {
|
|
62
|
+
if ((0, builtinAccess_1.isBuiltinStorageId)(connId, config)) {
|
|
63
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, connId, config, 'storage'))) {
|
|
64
|
+
return { error: 'Access to this built-in storage is not permitted', status: 403 };
|
|
65
|
+
}
|
|
66
|
+
const provider = config.storageProviders.find(p => p.id === connId);
|
|
67
|
+
if (!provider)
|
|
68
|
+
return { error: 'Storage provider not found', status: 404 };
|
|
69
|
+
return provider.type === 's3' ? new s3_1.S3Adapter(provider.config) : adapter;
|
|
70
|
+
}
|
|
71
|
+
const conn = project.storageConnections?.find((c) => c.id === connId);
|
|
72
|
+
if (!conn)
|
|
73
|
+
return { error: 'Storage connection not found', status: 404 };
|
|
74
|
+
if (!(0, encryption_1.isEncryptionKeySet)())
|
|
75
|
+
return { error: 'Encryption key not set', status: 500 };
|
|
76
|
+
const credential = project.credentials?.find((c) => c.id === conn.credentialId);
|
|
77
|
+
if (!credential)
|
|
78
|
+
return { error: 'Credential not found in project', status: 400 };
|
|
79
|
+
try {
|
|
80
|
+
const s3Config = JSON.parse((0, encryption_1.decrypt)(credential.encryptedValue));
|
|
81
|
+
return new s3_1.S3Adapter(s3Config);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
79
87
|
router.get('/upload-project-image-url/:id', async (req, res) => {
|
|
80
88
|
try {
|
|
81
89
|
const auth = req.auth;
|
|
@@ -301,7 +309,7 @@ function storageRouter(config) {
|
|
|
301
309
|
return res.status(404).json({ message: 'Project not found' });
|
|
302
310
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
303
311
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
304
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
312
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
305
313
|
if ('error' in connAdapter)
|
|
306
314
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
307
315
|
if (!connAdapter.getPresignedUploadUrl)
|
|
@@ -340,7 +348,7 @@ function storageRouter(config) {
|
|
|
340
348
|
return res.status(404).json({ message: 'Project not found' });
|
|
341
349
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
342
350
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
343
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
351
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
344
352
|
if ('error' in connAdapter)
|
|
345
353
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
346
354
|
const collection = getRawFilesCollection();
|
|
@@ -366,7 +374,7 @@ function storageRouter(config) {
|
|
|
366
374
|
return res.status(404).json({ message: 'Project not found' });
|
|
367
375
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
368
376
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
369
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
377
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
370
378
|
if ('error' in connAdapter)
|
|
371
379
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
372
380
|
const collection = getRawFilesCollection();
|
|
@@ -402,7 +410,7 @@ function storageRouter(config) {
|
|
|
402
410
|
if (!validateContentType(contentType, fileName, fileDoc.category)) {
|
|
403
411
|
return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
|
|
404
412
|
}
|
|
405
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
413
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
406
414
|
if ('error' in connAdapter)
|
|
407
415
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
408
416
|
if (!connAdapter.getPresignedUploadUrl) {
|
|
@@ -450,7 +458,7 @@ function storageRouter(config) {
|
|
|
450
458
|
return res.status(404).json({ message: 'Project not found' });
|
|
451
459
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
452
460
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
453
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
461
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
454
462
|
if ('error' in connAdapter)
|
|
455
463
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
456
464
|
const collection = getRawFilesCollection();
|
|
@@ -549,11 +557,14 @@ function storageRouter(config) {
|
|
|
549
557
|
return res.status(404).json({ message: 'Project not found' });
|
|
550
558
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
551
559
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
560
|
+
const isBuiltin = (0, builtinAccess_1.isBuiltinStorageId)(connId, config);
|
|
561
|
+
if (isBuiltin && !(await (0, builtinAccess_1.assertBuiltinAccess)(auth, connId, config, 'storage'))) {
|
|
562
|
+
return res.status(403).json({ message: 'Access to this built-in storage is not permitted' });
|
|
563
|
+
}
|
|
552
564
|
const collection = getRawFoldersCollection();
|
|
553
565
|
if (!collection)
|
|
554
566
|
return res.status(500).json({ message: 'Database not available' });
|
|
555
567
|
const folderId = crypto_1.default.randomUUID();
|
|
556
|
-
const isBuiltin = connId === 'builtin' || connId.startsWith('builtin-');
|
|
557
568
|
const folderDoc = {
|
|
558
569
|
_id: new mongodb_1.ObjectId(),
|
|
559
570
|
folderId,
|
|
@@ -717,6 +728,7 @@ function storageRouter(config) {
|
|
|
717
728
|
r2: { value: 'cloudflare-r2', name: 'Cloudflare R2' },
|
|
718
729
|
};
|
|
719
730
|
const storages = (config.storageProviders || []).map(provider => ({
|
|
731
|
+
id: provider.id,
|
|
720
732
|
...STORAGE_DISPLAY[provider.type],
|
|
721
733
|
type: provider.type,
|
|
722
734
|
}));
|
|
@@ -5,13 +5,8 @@ const express_1 = require("express");
|
|
|
5
5
|
const mongodb_1 = require("mongodb");
|
|
6
6
|
const manager_1 = require("../../models/manager");
|
|
7
7
|
const auth_1 = require("../../utils/auth");
|
|
8
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
8
9
|
const VALID_STORAGE_TYPES = ['s3', 'gcs', 'r2', 'azure'];
|
|
9
|
-
const STORAGE_DISPLAY = {
|
|
10
|
-
s3: 'AWS S3',
|
|
11
|
-
gcs: 'Google Cloud Storage',
|
|
12
|
-
r2: 'Cloudflare R2',
|
|
13
|
-
azure: 'Azure Blob Storage',
|
|
14
|
-
};
|
|
15
10
|
function storageConnectionRouter(config) {
|
|
16
11
|
const router = (0, express_1.Router)();
|
|
17
12
|
const Project = (0, manager_1.getModel)('projects');
|
|
@@ -24,17 +19,16 @@ function storageConnectionRouter(config) {
|
|
|
24
19
|
return res.status(404).json({ message: 'Project not found' });
|
|
25
20
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
26
21
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
isBuiltin: true,
|
|
22
|
+
const connections = project.storageConnections || [];
|
|
23
|
+
// Recompute availability live for builtins — never trust a stored flag, per
|
|
24
|
+
// "validate on every use" (TCORE-69). BYOC (source: 'integration') is always available.
|
|
25
|
+
const data = await Promise.all(connections.map(async (conn) => {
|
|
26
|
+
if (conn.source !== 'builtin')
|
|
27
|
+
return conn;
|
|
28
|
+
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
|
|
29
|
+
return { ...conn, isBuiltin: true, available };
|
|
35
30
|
}));
|
|
36
|
-
|
|
37
|
-
return res.status(200).json({ data: [...builtinConnections, ...projectConnections] });
|
|
31
|
+
return res.status(200).json({ data });
|
|
38
32
|
}
|
|
39
33
|
catch (err) {
|
|
40
34
|
res.status(500).json({ message: 'Failed to fetch storage connections', details: err.message });
|
|
@@ -82,7 +76,7 @@ function storageConnectionRouter(config) {
|
|
|
82
76
|
router.delete('/projects/:id/connections/storage/:connId', async (req, res) => {
|
|
83
77
|
try {
|
|
84
78
|
const { connId } = req.params;
|
|
85
|
-
if (
|
|
79
|
+
if ((0, builtinAccess_1.isBuiltinStorageId)(connId, config)) {
|
|
86
80
|
return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
|
|
87
81
|
}
|
|
88
82
|
const auth = req.auth;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.userIntegrationRouter = userIntegrationRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
6
|
+
const userIntegration_1 = require("../../services/userIntegration");
|
|
7
|
+
const sanitize_1 = require("../../utils/sanitize");
|
|
8
|
+
const VALID_KINDS = ['database', 'storage'];
|
|
9
|
+
const VALID_DB_PROVIDERS = ['postgresql', 'mongodb'];
|
|
10
|
+
const VALID_STORAGE_PROVIDERS = ['s3', 'gcs', 'r2', 'azure'];
|
|
11
|
+
function userIntegrationRouter(config) {
|
|
12
|
+
const router = (0, express_1.Router)();
|
|
13
|
+
// ─── Combined pool: account integrations + built-ins the user can access ─────
|
|
14
|
+
router.get('/user/integrations', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const auth = req.auth;
|
|
17
|
+
const owned = await (0, userIntegration_1.listUserIntegrations)(auth.userId);
|
|
18
|
+
const pool = owned.map(i => ({
|
|
19
|
+
id: i.id, kind: i.kind, name: i.name, provider: i.provider, source: 'integration', available: true,
|
|
20
|
+
}));
|
|
21
|
+
for (const db of config.databases ?? []) {
|
|
22
|
+
// requiredPlan is not populated here — canUseBuiltin only returns a boolean today,
|
|
23
|
+
// so there's no plan-level detail to surface without a hook-signature change.
|
|
24
|
+
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, db.id, config, 'database');
|
|
25
|
+
pool.push({ id: db.id, kind: 'database', name: db.id, provider: db.type, source: 'builtin', available });
|
|
26
|
+
}
|
|
27
|
+
for (const provider of config.storageProviders ?? []) {
|
|
28
|
+
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, provider.id, config, 'storage');
|
|
29
|
+
pool.push({ id: provider.id, kind: 'storage', name: provider.id, provider: provider.type, source: 'builtin', available });
|
|
30
|
+
}
|
|
31
|
+
res.json({ data: pool });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
res.status(500).json({ message: 'Failed to list integrations', details: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// ─── Create ────────────────────────────────────────────────────────────────
|
|
38
|
+
router.post('/user/integrations', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const auth = req.auth;
|
|
41
|
+
const { kind, provider, name, description, credential } = req.body;
|
|
42
|
+
if (!kind || !VALID_KINDS.includes(kind)) {
|
|
43
|
+
return res.status(400).json({ message: `kind must be one of: ${VALID_KINDS.join(', ')}` });
|
|
44
|
+
}
|
|
45
|
+
const validProviders = kind === 'database' ? VALID_DB_PROVIDERS : VALID_STORAGE_PROVIDERS;
|
|
46
|
+
if (!provider || !validProviders.includes(provider)) {
|
|
47
|
+
return res.status(400).json({ message: `provider must be one of: ${validProviders.join(', ')}` });
|
|
48
|
+
}
|
|
49
|
+
if (!name)
|
|
50
|
+
return res.status(400).json({ message: 'name is required' });
|
|
51
|
+
if (credential === undefined || credential === null) {
|
|
52
|
+
return res.status(400).json({ message: 'credential is required' });
|
|
53
|
+
}
|
|
54
|
+
const integration = await (0, userIntegration_1.createUserIntegration)(auth.userId, {
|
|
55
|
+
kind, provider, name, description, credentialValue: credential,
|
|
56
|
+
});
|
|
57
|
+
res.status(201).json({ data: (0, sanitize_1.sanitizeUserIntegration)(integration) });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
res.status(500).json({ message: 'Failed to create integration', details: err.message });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// ─── Update ────────────────────────────────────────────────────────────────
|
|
64
|
+
router.patch('/user/integrations/:integrationId', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const auth = req.auth;
|
|
67
|
+
const { name, description, credential } = req.body;
|
|
68
|
+
const updated = await (0, userIntegration_1.updateUserIntegration)(auth.userId, req.params.integrationId, {
|
|
69
|
+
name, description, credentialValue: credential,
|
|
70
|
+
});
|
|
71
|
+
if (!updated)
|
|
72
|
+
return res.status(404).json({ message: 'Integration not found' });
|
|
73
|
+
res.json({ data: (0, sanitize_1.sanitizeUserIntegration)(updated) });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
res.status(500).json({ message: 'Failed to update integration', details: err.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// ─── Delete ────────────────────────────────────────────────────────────────
|
|
80
|
+
router.delete('/user/integrations/:integrationId', async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const auth = req.auth;
|
|
83
|
+
const deleted = await (0, userIntegration_1.deleteUserIntegration)(auth.userId, req.params.integrationId);
|
|
84
|
+
if (!deleted)
|
|
85
|
+
return res.status(404).json({ message: 'Integration not found' });
|
|
86
|
+
res.json({ message: 'Integration deleted' });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
res.status(500).json({ message: 'Failed to delete integration', details: err.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return router;
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.backfillBuiltinConnections = backfillBuiltinConnections;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const manager_1 = require("../models/manager");
|
|
9
|
+
// Pre-BYOC, every project appeared connected to every configured builtin because
|
|
10
|
+
// storageConnection.ts/dbConnection.ts SYNTHESIZED builtins at read time instead of
|
|
11
|
+
// persisting them (see storageConnection.ts's old GET handler). Now that connections must
|
|
12
|
+
// be real rows on the project, this seeds those rows once for projects that predate the
|
|
13
|
+
// change — so existing projects don't silently lose access on deploy.
|
|
14
|
+
//
|
|
15
|
+
// Guarded by ProjectInfo.builtinBackfilledAt: runs once per project, ever. An admin who
|
|
16
|
+
// later disconnects a builtin on purpose won't have it silently reappear on the next boot.
|
|
17
|
+
async function backfillBuiltinConnections(config) {
|
|
18
|
+
if (!config.databases?.length && !config.storageProviders?.length)
|
|
19
|
+
return;
|
|
20
|
+
const Project = (0, manager_1.getModel)('projects');
|
|
21
|
+
const allProjects = await Project.find({});
|
|
22
|
+
const pending = allProjects.filter(p => !p.builtinBackfilledAt);
|
|
23
|
+
if (!pending.length)
|
|
24
|
+
return;
|
|
25
|
+
for (const project of pending) {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const dbConnections = (config.databases ?? []).map(db => ({
|
|
28
|
+
id: crypto_1.default.randomUUID(),
|
|
29
|
+
name: db.id,
|
|
30
|
+
dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
|
|
31
|
+
credentialId: '',
|
|
32
|
+
projectId: project.id,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
isBuiltin: true,
|
|
35
|
+
integrationId: db.id,
|
|
36
|
+
source: 'builtin',
|
|
37
|
+
}));
|
|
38
|
+
const storageConnections = (config.storageProviders ?? []).map(provider => ({
|
|
39
|
+
id: crypto_1.default.randomUUID(),
|
|
40
|
+
name: provider.id,
|
|
41
|
+
type: provider.type,
|
|
42
|
+
credentialId: '',
|
|
43
|
+
projectId: project.id,
|
|
44
|
+
createdAt: now,
|
|
45
|
+
isBuiltin: true,
|
|
46
|
+
integrationId: provider.id,
|
|
47
|
+
source: 'builtin',
|
|
48
|
+
}));
|
|
49
|
+
await Project.update({ _id: project.id }, {
|
|
50
|
+
dbConnections: [...(project.dbConnections ?? []), ...dbConnections],
|
|
51
|
+
storageConnections: [...(project.storageConnections ?? []), ...storageConnections],
|
|
52
|
+
builtinBackfilledAt: now,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
console.log(`✅ Backfilled built-in connections for ${pending.length} project(s).`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IntegrationKind, UserIntegration } from '../types';
|
|
2
|
+
export declare function listUserIntegrations(userId: string): Promise<UserIntegration[]>;
|
|
3
|
+
export declare function createUserIntegration(userId: string, input: {
|
|
4
|
+
kind: IntegrationKind;
|
|
5
|
+
provider: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
credentialValue: unknown;
|
|
9
|
+
}): Promise<UserIntegration>;
|
|
10
|
+
export declare function updateUserIntegration(userId: string, integrationId: string, updates: {
|
|
11
|
+
name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
credentialValue?: unknown;
|
|
14
|
+
}): Promise<UserIntegration | null>;
|
|
15
|
+
export declare function deleteUserIntegration(userId: string, integrationId: string): Promise<boolean>;
|
|
16
|
+
export declare function getDecryptedIntegrationCredential(userId: string, integrationId: string): Promise<unknown | null>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listUserIntegrations = listUserIntegrations;
|
|
7
|
+
exports.createUserIntegration = createUserIntegration;
|
|
8
|
+
exports.updateUserIntegration = updateUserIntegration;
|
|
9
|
+
exports.deleteUserIntegration = deleteUserIntegration;
|
|
10
|
+
exports.getDecryptedIntegrationCredential = getDecryptedIntegrationCredential;
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const manager_1 = require("../models/manager");
|
|
13
|
+
const encryption_1 = require("../utils/encryption");
|
|
14
|
+
function encryptCredential(kind, value) {
|
|
15
|
+
return kind === 'storage' ? (0, encryption_1.encrypt)(JSON.stringify(value)) : (0, encryption_1.encrypt)(String(value));
|
|
16
|
+
}
|
|
17
|
+
async function listUserIntegrations(userId) {
|
|
18
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
19
|
+
return UserIntegrationModel.find({ userId });
|
|
20
|
+
}
|
|
21
|
+
async function createUserIntegration(userId, input) {
|
|
22
|
+
if (!(0, encryption_1.isEncryptionKeySet)()) {
|
|
23
|
+
throw new Error('Encryption key is not set. Cannot create integration.');
|
|
24
|
+
}
|
|
25
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const integration = {
|
|
28
|
+
id: crypto_1.default.randomUUID(),
|
|
29
|
+
userId,
|
|
30
|
+
kind: input.kind,
|
|
31
|
+
provider: input.provider,
|
|
32
|
+
name: input.name,
|
|
33
|
+
description: input.description,
|
|
34
|
+
encryptedCredential: encryptCredential(input.kind, input.credentialValue),
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
};
|
|
38
|
+
return UserIntegrationModel.create(integration);
|
|
39
|
+
}
|
|
40
|
+
async function updateUserIntegration(userId, integrationId, updates) {
|
|
41
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
42
|
+
const existing = await UserIntegrationModel.findOne({ id: integrationId, userId });
|
|
43
|
+
if (!existing)
|
|
44
|
+
return null;
|
|
45
|
+
const $set = { updatedAt: new Date() };
|
|
46
|
+
if (updates.name !== undefined)
|
|
47
|
+
$set.name = updates.name;
|
|
48
|
+
if (updates.description !== undefined)
|
|
49
|
+
$set.description = updates.description;
|
|
50
|
+
if (updates.credentialValue !== undefined) {
|
|
51
|
+
if (!(0, encryption_1.isEncryptionKeySet)()) {
|
|
52
|
+
throw new Error('Encryption key is not set. Cannot update integration.');
|
|
53
|
+
}
|
|
54
|
+
$set.encryptedCredential = encryptCredential(existing.kind, updates.credentialValue);
|
|
55
|
+
}
|
|
56
|
+
return UserIntegrationModel.update({ id: integrationId, userId }, $set);
|
|
57
|
+
}
|
|
58
|
+
async function deleteUserIntegration(userId, integrationId) {
|
|
59
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
60
|
+
const deletedCount = await UserIntegrationModel.delete({ id: integrationId, userId });
|
|
61
|
+
return deletedCount > 0;
|
|
62
|
+
}
|
|
63
|
+
async function getDecryptedIntegrationCredential(userId, integrationId) {
|
|
64
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
65
|
+
const integration = await UserIntegrationModel.findOne({ id: integrationId, userId });
|
|
66
|
+
if (!integration)
|
|
67
|
+
return null;
|
|
68
|
+
if (!(0, encryption_1.isEncryptionKeySet)()) {
|
|
69
|
+
throw new Error('Encryption key is not set. Cannot decrypt integration credential.');
|
|
70
|
+
}
|
|
71
|
+
const decrypted = (0, encryption_1.decrypt)(integration.encryptedCredential);
|
|
72
|
+
return integration.kind === 'storage' ? JSON.parse(decrypted) : decrypted;
|
|
73
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,8 +17,12 @@ export interface StorageConnection {
|
|
|
17
17
|
createdAt: Date;
|
|
18
18
|
description?: string;
|
|
19
19
|
isBuiltin?: boolean;
|
|
20
|
+
integrationId?: string;
|
|
21
|
+
source?: 'builtin' | 'integration';
|
|
22
|
+
available?: boolean;
|
|
20
23
|
}
|
|
21
24
|
export type StorageProvider = {
|
|
25
|
+
id: string;
|
|
22
26
|
type: StorageProviderType;
|
|
23
27
|
config: S3Config;
|
|
24
28
|
};
|
|
@@ -74,6 +78,7 @@ export interface ProjectInfo {
|
|
|
74
78
|
dbConnections?: DbConnection[];
|
|
75
79
|
storageConnections?: StorageConnection[];
|
|
76
80
|
pipelines?: PipelineRef[];
|
|
81
|
+
builtinBackfilledAt?: Date;
|
|
77
82
|
}
|
|
78
83
|
export interface Credential {
|
|
79
84
|
id: string;
|
|
@@ -107,10 +112,7 @@ export interface DatabaseCredential {
|
|
|
107
112
|
main?: boolean;
|
|
108
113
|
}
|
|
109
114
|
export interface StreamByConfig {
|
|
110
|
-
storageProviders:
|
|
111
|
-
type: StorageProviderType;
|
|
112
|
-
config: S3Config;
|
|
113
|
-
}[];
|
|
115
|
+
storageProviders: StorageProvider[];
|
|
114
116
|
authProvider: AuthProvider;
|
|
115
117
|
databases?: DatabaseCredential[];
|
|
116
118
|
adapter?: StorageAdapter;
|
|
@@ -118,6 +120,7 @@ export interface StreamByConfig {
|
|
|
118
120
|
websocket?: {
|
|
119
121
|
server: WebSocketServer;
|
|
120
122
|
};
|
|
123
|
+
canUseBuiltin?: (auth: Auth, builtinId: string, kind: 'database' | 'storage') => boolean | Promise<boolean>;
|
|
121
124
|
}
|
|
122
125
|
export interface Notification {
|
|
123
126
|
_id?: string;
|
|
@@ -154,6 +157,21 @@ export interface DbConnection {
|
|
|
154
157
|
projectId: string;
|
|
155
158
|
createdAt: Date;
|
|
156
159
|
description?: string;
|
|
160
|
+
isBuiltin?: boolean;
|
|
161
|
+
integrationId?: string;
|
|
162
|
+
source?: 'builtin' | 'integration';
|
|
163
|
+
}
|
|
164
|
+
export type IntegrationKind = 'database' | 'storage';
|
|
165
|
+
export interface UserIntegration {
|
|
166
|
+
id: string;
|
|
167
|
+
userId: string;
|
|
168
|
+
kind: IntegrationKind;
|
|
169
|
+
provider: ExternalDbType | StorageProviderType;
|
|
170
|
+
name: string;
|
|
171
|
+
description?: string;
|
|
172
|
+
encryptedCredential: string;
|
|
173
|
+
createdAt: Date;
|
|
174
|
+
updatedAt: Date;
|
|
157
175
|
}
|
|
158
176
|
export interface ColumnDefinition {
|
|
159
177
|
name: string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Auth, StreamByConfig } from '../types';
|
|
2
|
+
export declare function assertBuiltinAccess(auth: Auth, builtinId: string, config: StreamByConfig, kind: 'database' | 'storage'): Promise<boolean>;
|
|
3
|
+
export declare function isBuiltinStorageId(id: string, config: StreamByConfig): boolean;
|
|
4
|
+
export declare function isBuiltinDb(id: string, config: StreamByConfig): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertBuiltinAccess = assertBuiltinAccess;
|
|
4
|
+
exports.isBuiltinStorageId = isBuiltinStorageId;
|
|
5
|
+
exports.isBuiltinDb = isBuiltinDb;
|
|
6
|
+
// Gates access to a built-in database/storage provider. Delegates to config.canUseBuiltin
|
|
7
|
+
// when the mounting app implements it (e.g. Nhexa-API checking user_subscriptions);
|
|
8
|
+
// defaults to allow when absent, preserving pre-BYOC behavior for deploys without gating.
|
|
9
|
+
async function assertBuiltinAccess(auth, builtinId, config, kind) {
|
|
10
|
+
if (!config.canUseBuiltin)
|
|
11
|
+
return true;
|
|
12
|
+
return config.canUseBuiltin(auth, builtinId, kind);
|
|
13
|
+
}
|
|
14
|
+
function isBuiltinStorageId(id, config) {
|
|
15
|
+
return (config.storageProviders ?? []).some(p => p.id === id);
|
|
16
|
+
}
|
|
17
|
+
function isBuiltinDb(id, config) {
|
|
18
|
+
return (config.databases ?? []).some(db => db.id === id);
|
|
19
|
+
}
|
package/dist/utils/sanitize.d.ts
CHANGED
package/dist/utils/sanitize.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sanitizeProject = sanitizeProject;
|
|
4
|
+
exports.sanitizeUserIntegration = sanitizeUserIntegration;
|
|
4
5
|
function sanitizeProject(project) {
|
|
5
6
|
if (!project)
|
|
6
7
|
return project;
|
|
@@ -9,3 +10,9 @@ function sanitizeProject(project) {
|
|
|
9
10
|
credentials: project.credentials?.map(({ id, key }) => ({ id, key })) ?? [],
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
function sanitizeUserIntegration(integration) {
|
|
14
|
+
if (!integration)
|
|
15
|
+
return integration;
|
|
16
|
+
const { encryptedCredential, ...rest } = integration;
|
|
17
|
+
return rest;
|
|
18
|
+
}
|