streamby-core 0.29.9 → 0.29.12

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.
@@ -81,6 +81,7 @@ const ensureTablesExist = async (pool) => {
81
81
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
82
82
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
83
83
  ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
84
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
84
85
  `);
85
86
  // Ensure dbType exists with a non-null default
86
87
  await pool.query(`
@@ -40,24 +40,41 @@ const manager_1 = require("../../models/manager");
40
40
  const auth_1 = require("../../utils/auth");
41
41
  const connectionManager_1 = require("../../adapters/database/connectionManager");
42
42
  const builtinAccess_1 = require("../../utils/builtinAccess");
43
+ const userIntegration_1 = require("../../services/userIntegration");
43
44
  const dbConnection_1 = require("../../services/dbConnection");
44
45
  const VALID_DB_TYPES = ['postgresql', 'mongodb'];
45
- function resolveBuiltinConnection(connId, config) {
46
- const db = (config.databases ?? []).find(d => d.id === connId);
46
+ // `builtinId` is config.databases[].id (e.g. 'mongo'), NOT the DbConnection row's own id —
47
+ // those are different values since TCORE-69's backfill gives each row its own UUID.
48
+ function resolveBuiltinConnection(builtinId, config) {
49
+ const db = (config.databases ?? []).find(d => d.id === builtinId);
47
50
  if (!db)
48
- return { error: `Database '${connId}' not found in config`, status: 404 };
51
+ return { error: `Database '${builtinId}' not found in config`, status: 404 };
49
52
  try {
50
- const { client, type } = (0, connectionManager_1.getConnection)(connId);
53
+ const { client, type } = (0, connectionManager_1.getConnection)(builtinId);
51
54
  return { client: client, dbType: type };
52
55
  }
53
56
  catch (e) {
54
57
  return { error: e.message, status: 503 };
55
58
  }
56
59
  }
60
+ // Finds the connection row + classifies it. A row is builtin iff conn.source === 'builtin'
61
+ // (persisted at creation/backfill time) — NOT by comparing the row's own id against
62
+ // config.databases, which no longer holds since builtin rows get their own generated id.
63
+ function findDbConnection(project, connId) {
64
+ return project.dbConnections?.find((c) => c.id === connId);
65
+ }
57
66
  async function getDecryptedConnectionString(project, connId) {
58
67
  const conn = project.dbConnections?.find((c) => c.id === connId);
59
68
  if (!conn)
60
69
  return { error: 'DB connection not found', status: 404 };
70
+ if (conn.source === 'integration') {
71
+ if (!conn.integrationId)
72
+ return { error: 'Connection is missing its integrationId', status: 500 };
73
+ const credential = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
74
+ if (!credential)
75
+ return { error: 'Integration not found', status: 400 };
76
+ return { connectionString: credential, conn };
77
+ }
61
78
  const { decrypt, isEncryptionKeySet } = await Promise.resolve().then(() => __importStar(require('../../utils/encryption')));
62
79
  if (!isEncryptionKeySet())
63
80
  return { error: 'Encryption key is not set', status: 500 };
@@ -91,9 +108,15 @@ function dbConnectionRouter(config) {
91
108
  if (auth.role !== 'admin' && auth.role !== 'editor') {
92
109
  return res.status(403).json({ message: 'Permission denied' });
93
110
  }
94
- const { name, dbType, credentialId, description } = req.body;
95
- if (!name || !dbType || !credentialId) {
96
- return res.status(400).json({ message: 'name, dbType, and credentialId are required' });
111
+ const { name, dbType, credentialId, integrationId, description } = req.body;
112
+ if (!name || !dbType) {
113
+ return res.status(400).json({ message: 'name and dbType are required' });
114
+ }
115
+ if (!credentialId && !integrationId) {
116
+ return res.status(400).json({ message: 'Either credentialId or integrationId is required' });
117
+ }
118
+ if (credentialId && integrationId) {
119
+ return res.status(400).json({ message: 'Provide only one of credentialId or integrationId' });
97
120
  }
98
121
  if (!VALID_DB_TYPES.includes(dbType)) {
99
122
  return res.status(400).json({ message: `dbType must be one of: ${VALID_DB_TYPES.join(', ')}` });
@@ -103,16 +126,28 @@ function dbConnectionRouter(config) {
103
126
  return res.status(404).json({ message: 'Project not found' });
104
127
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
105
128
  return res.status(403).json({ message: 'Unauthorized project access' });
106
- const credExists = project.credentials?.some((c) => c.id === credentialId);
107
- if (!credExists)
108
- return res.status(400).json({ message: 'Credential not found in project' });
129
+ if (integrationId) {
130
+ const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
131
+ const integration = await UserIntegrationModel.findOne({ id: integrationId, userId: auth.userId });
132
+ if (!integration)
133
+ return res.status(404).json({ message: 'Integration not found' });
134
+ if (integration.kind !== 'database')
135
+ return res.status(400).json({ message: 'Integration is not a database integration' });
136
+ }
137
+ else {
138
+ const credExists = project.credentials?.some((c) => c.id === credentialId);
139
+ if (!credExists)
140
+ return res.status(400).json({ message: 'Credential not found in project' });
141
+ }
109
142
  const connection = {
110
143
  id: new mongodb_1.ObjectId().toHexString(),
111
144
  name,
112
145
  dbType,
113
- credentialId,
146
+ credentialId: credentialId ?? '',
114
147
  projectId: req.params.id,
115
148
  createdAt: new Date(),
149
+ source: integrationId ? 'integration' : 'manual',
150
+ ...(integrationId && { integrationId }),
116
151
  ...(description !== undefined && { description }),
117
152
  };
118
153
  await Project.update({ _id: req.params.id }, { $push: { dbConnections: connection } });
@@ -125,9 +160,6 @@ function dbConnectionRouter(config) {
125
160
  // ─── Delete DB connection ─────────────────────────────────────────────────
126
161
  router.delete('/projects/:id/connections/db/:connId', async (req, res) => {
127
162
  try {
128
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
129
- return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
130
- }
131
163
  const auth = req.auth;
132
164
  if (auth.role !== 'admin' && auth.role !== 'editor') {
133
165
  return res.status(403).json({ message: 'Permission denied' });
@@ -137,9 +169,12 @@ function dbConnectionRouter(config) {
137
169
  return res.status(404).json({ message: 'Project not found' });
138
170
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
139
171
  return res.status(403).json({ message: 'Unauthorized project access' });
140
- const exists = project.dbConnections?.some((c) => c.id === req.params.connId);
141
- if (!exists)
172
+ const conn = findDbConnection(project, req.params.connId);
173
+ if (!conn)
142
174
  return res.status(404).json({ message: 'DB connection not found' });
175
+ if (conn.source === 'builtin') {
176
+ return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
177
+ }
143
178
  await Project.update({ _id: req.params.id }, { $pull: { dbConnections: { id: req.params.connId } } });
144
179
  return res.status(200).json({ message: 'DB connection deleted successfully' });
145
180
  }
@@ -156,11 +191,15 @@ function dbConnectionRouter(config) {
156
191
  return res.status(404).json({ message: 'Project not found' });
157
192
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
158
193
  return res.status(403).json({ message: 'Unauthorized project access' });
159
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
160
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
194
+ const conn = findDbConnection(project, req.params.connId);
195
+ if (!conn)
196
+ return res.status(404).json({ message: 'DB connection not found' });
197
+ if (conn.source === 'builtin') {
198
+ const builtinId = conn.integrationId;
199
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
161
200
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
162
201
  }
163
- const internal = resolveBuiltinConnection(req.params.connId, config);
202
+ const internal = resolveBuiltinConnection(builtinId, config);
164
203
  if ('error' in internal)
165
204
  return res.status(internal.status).json({ message: internal.error });
166
205
  const tables = await (0, dbConnection_1.listTablesInternal)(internal.client, internal.dbType, req.params.id);
@@ -191,11 +230,15 @@ function dbConnectionRouter(config) {
191
230
  const schema = req.body;
192
231
  if (!schema.tableName)
193
232
  return res.status(400).json({ message: 'tableName is required' });
194
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
195
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
233
+ const conn = findDbConnection(project, req.params.connId);
234
+ if (!conn)
235
+ return res.status(404).json({ message: 'DB connection not found' });
236
+ if (conn.source === 'builtin') {
237
+ const builtinId = conn.integrationId;
238
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
196
239
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
197
240
  }
198
- const internal = resolveBuiltinConnection(req.params.connId, config);
241
+ const internal = resolveBuiltinConnection(builtinId, config);
199
242
  if ('error' in internal)
200
243
  return res.status(internal.status).json({ message: internal.error });
201
244
  if (internal.dbType === 'sql' && (!Array.isArray(schema.columns) || schema.columns.length === 0)) {
@@ -229,11 +272,15 @@ function dbConnectionRouter(config) {
229
272
  return res.status(404).json({ message: 'Project not found' });
230
273
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
231
274
  return res.status(403).json({ message: 'Unauthorized project access' });
232
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
233
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
275
+ const conn = findDbConnection(project, req.params.connId);
276
+ if (!conn)
277
+ return res.status(404).json({ message: 'DB connection not found' });
278
+ if (conn.source === 'builtin') {
279
+ const builtinId = conn.integrationId;
280
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
234
281
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
235
282
  }
236
- const internal = resolveBuiltinConnection(req.params.connId, config);
283
+ const internal = resolveBuiltinConnection(builtinId, config);
237
284
  if ('error' in internal)
238
285
  return res.status(internal.status).json({ message: internal.error });
239
286
  await (0, dbConnection_1.deleteTableOrCollectionInternal)(internal.client, internal.dbType, req.params.tableName, req.params.id);
@@ -260,11 +307,15 @@ function dbConnectionRouter(config) {
260
307
  return res.status(403).json({ message: 'Unauthorized project access' });
261
308
  const limit = Math.min(parseInt(String(req.query.limit ?? 50), 10), 500);
262
309
  const offset = Math.max(parseInt(String(req.query.offset ?? 0), 10), 0);
263
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
264
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
310
+ const conn = findDbConnection(project, req.params.connId);
311
+ if (!conn)
312
+ return res.status(404).json({ message: 'DB connection not found' });
313
+ if (conn.source === 'builtin') {
314
+ const builtinId = conn.integrationId;
315
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
265
316
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
266
317
  }
267
- const internal = resolveBuiltinConnection(req.params.connId, config);
318
+ const internal = resolveBuiltinConnection(builtinId, config);
268
319
  if ('error' in internal)
269
320
  return res.status(internal.status).json({ message: internal.error });
270
321
  const records = await (0, dbConnection_1.queryRecordsInternal)(internal.client, internal.dbType, req.params.tableName, limit, offset, req.params.id);
@@ -296,11 +347,15 @@ function dbConnectionRouter(config) {
296
347
  if (!record || typeof record !== 'object' || Array.isArray(record)) {
297
348
  return res.status(400).json({ message: 'Request body must be a JSON object' });
298
349
  }
299
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
300
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
350
+ const conn = findDbConnection(project, req.params.connId);
351
+ if (!conn)
352
+ return res.status(404).json({ message: 'DB connection not found' });
353
+ if (conn.source === 'builtin') {
354
+ const builtinId = conn.integrationId;
355
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
301
356
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
302
357
  }
303
- const internal = resolveBuiltinConnection(req.params.connId, config);
358
+ const internal = resolveBuiltinConnection(builtinId, config);
304
359
  if ('error' in internal)
305
360
  return res.status(internal.status).json({ message: internal.error });
306
361
  const inserted = await (0, dbConnection_1.insertRecordInternal)(internal.client, internal.dbType, req.params.tableName, record, req.params.id);
@@ -331,11 +386,15 @@ function dbConnectionRouter(config) {
331
386
  if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
332
387
  return res.status(400).json({ message: 'Request body must be a JSON object' });
333
388
  }
334
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
335
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
389
+ const conn = findDbConnection(project, req.params.connId);
390
+ if (!conn)
391
+ return res.status(404).json({ message: 'DB connection not found' });
392
+ if (conn.source === 'builtin') {
393
+ const builtinId = conn.integrationId;
394
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
336
395
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
337
396
  }
338
- const internal = resolveBuiltinConnection(req.params.connId, config);
397
+ const internal = resolveBuiltinConnection(builtinId, config);
339
398
  if ('error' in internal)
340
399
  return res.status(internal.status).json({ message: internal.error });
341
400
  const updated = await (0, dbConnection_1.updateRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, updates, req.params.id);
@@ -362,11 +421,15 @@ function dbConnectionRouter(config) {
362
421
  return res.status(404).json({ message: 'Project not found' });
363
422
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
364
423
  return res.status(403).json({ message: 'Unauthorized project access' });
365
- if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
366
- if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, req.params.connId, config, 'database'))) {
424
+ const conn = findDbConnection(project, req.params.connId);
425
+ if (!conn)
426
+ return res.status(404).json({ message: 'DB connection not found' });
427
+ if (conn.source === 'builtin') {
428
+ const builtinId = conn.integrationId;
429
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
367
430
  return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
368
431
  }
369
- const internal = resolveBuiltinConnection(req.params.connId, config);
432
+ const internal = resolveBuiltinConnection(builtinId, config);
370
433
  if ('error' in internal)
371
434
  return res.status(internal.status).json({ message: internal.error });
372
435
  await (0, dbConnection_1.deleteRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, req.params.id);
@@ -1,17 +1,13 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.projectRouter = projectRouter;
7
4
  const express_1 = require("express");
8
- const crypto_1 = __importDefault(require("crypto"));
9
5
  const manager_1 = require("../../models/manager");
10
6
  const auth_1 = require("../../utils/auth");
11
7
  const connectionManager_1 = require("../../adapters/database/connectionManager");
12
8
  const sql_1 = require("../../adapters/database/sql");
13
9
  const sanitize_1 = require("../../utils/sanitize");
14
- const builtinAccess_1 = require("../../utils/builtinAccess");
10
+ const projectIntegration_1 = require("../../services/projectIntegration");
15
11
  function projectRouter(config) {
16
12
  const router = (0, express_1.Router)();
17
13
  const Project = (0, manager_1.getModel)('projects');
@@ -98,47 +94,14 @@ function projectRouter(config) {
98
94
  const dbConnections = [];
99
95
  const storageConnections = [];
100
96
  if (Array.isArray(integrationIds)) {
101
- const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
102
97
  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
- }
98
+ const result = await (0, projectIntegration_1.classifyIntegrationId)(id, auth, config, '');
99
+ if ('error' in result)
100
+ return res.status(result.status).json({ message: result.error });
101
+ if (result.kind === 'database')
102
+ dbConnections.push(result.connection);
103
+ else
104
+ storageConnections.push(result.connection);
142
105
  }
143
106
  }
144
107
  let newProject = await Project.create({
@@ -430,5 +393,68 @@ function projectRouter(config) {
430
393
  res.status(500).json({ message: 'Failed to update project origins', details: err.message });
431
394
  }
432
395
  });
396
+ // ─── Add an integration to an existing project ───────────────────────────
397
+ router.post('/projects/:id/integrations', async (req, res) => {
398
+ try {
399
+ const auth = req.auth;
400
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
401
+ return res.status(403).json({ message: 'Permission denied' });
402
+ }
403
+ const projectId = req.params.id;
404
+ const { integrationId } = req.body;
405
+ if (!integrationId)
406
+ return res.status(400).json({ message: 'integrationId is required' });
407
+ const project = await Project.findOne({ _id: projectId });
408
+ if (!project)
409
+ return res.status(404).json({ message: 'Project not found' });
410
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
411
+ return res.status(403).json({ message: 'Unauthorized project access' });
412
+ const alreadyConnected = [...(project.dbConnections ?? []), ...(project.storageConnections ?? [])]
413
+ .some((c) => c.integrationId === integrationId);
414
+ if (alreadyConnected)
415
+ return res.status(400).json({ message: 'This integration is already connected to the project' });
416
+ const result = await (0, projectIntegration_1.classifyIntegrationId)(integrationId, auth, config, projectId);
417
+ if ('error' in result)
418
+ return res.status(result.status).json({ message: result.error });
419
+ const updated = result.kind === 'database'
420
+ ? await Project.update({ _id: projectId }, { $push: { dbConnections: result.connection } })
421
+ : await Project.update({ _id: projectId }, { $push: { storageConnections: result.connection } });
422
+ if (!updated)
423
+ return res.status(404).json({ message: 'Project not found or not updated' });
424
+ res.status(201).json({ data: result.connection, project: (0, sanitize_1.sanitizeProject)(updated) });
425
+ }
426
+ catch (err) {
427
+ res.status(500).json({ message: 'Failed to add integration', details: err.message });
428
+ }
429
+ });
430
+ // ─── Remove an integration from a project ────────────────────────────────
431
+ router.delete('/projects/:id/integrations/:integrationId', async (req, res) => {
432
+ try {
433
+ const auth = req.auth;
434
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
435
+ return res.status(403).json({ message: 'Permission denied' });
436
+ }
437
+ const projectId = req.params.id;
438
+ const { integrationId } = req.params;
439
+ const project = await Project.findOne({ _id: projectId });
440
+ if (!project)
441
+ return res.status(404).json({ message: 'Project not found' });
442
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
443
+ return res.status(403).json({ message: 'Unauthorized project access' });
444
+ const dbConn = (project.dbConnections ?? []).find((c) => c.integrationId === integrationId);
445
+ const storageConn = (project.storageConnections ?? []).find((c) => c.integrationId === integrationId);
446
+ if (!dbConn && !storageConn)
447
+ return res.status(404).json({ message: 'Integration not connected to this project' });
448
+ const updated = dbConn
449
+ ? await Project.update({ _id: projectId }, { $pull: { dbConnections: { id: dbConn.id } } })
450
+ : await Project.update({ _id: projectId }, { $pull: { storageConnections: { id: storageConn.id } } });
451
+ if (!updated)
452
+ return res.status(404).json({ message: 'Project not found or not updated' });
453
+ res.status(200).json({ message: 'Integration removed', project: (0, sanitize_1.sanitizeProject)(updated) });
454
+ }
455
+ catch (err) {
456
+ res.status(500).json({ message: 'Failed to remove integration', details: err.message });
457
+ }
458
+ });
433
459
  return router;
434
460
  }
@@ -14,6 +14,7 @@ const connectionManager_1 = require("../../adapters/database/connectionManager")
14
14
  const auth_1 = require("../../utils/auth");
15
15
  const encryption_1 = require("../../utils/encryption");
16
16
  const builtinAccess_1 = require("../../utils/builtinAccess");
17
+ const userIntegration_1 = require("../../services/userIntegration");
17
18
  const mongodb_1 = require("mongodb");
18
19
  const crypto_1 = __importDefault(require("crypto"));
19
20
  const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
@@ -51,26 +52,50 @@ function storageRouter(config) {
51
52
  const router = (0, express_1.Router)();
52
53
  const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
53
54
  const Project = (0, manager_1.getModel)('projects');
54
- function connFilter(projectId, connId, extra) {
55
- const isBuiltin = (0, builtinAccess_1.isBuiltinStorageId)(connId, config);
55
+ // A row is builtin iff its own `source === 'builtin'` (set at backfill/creation time) —
56
+ // NOT by comparing connId (the row's own generated id) against config.storageProviders,
57
+ // which no longer holds since builtin rows get their own id distinct from provider.id.
58
+ function findStorageConn(project, connId) {
59
+ return project.storageConnections?.find((c) => c.id === connId);
60
+ }
61
+ function connFilter(projectId, connId, project, extra) {
62
+ const conn = findStorageConn(project, connId);
63
+ const isBuiltin = conn?.source === 'builtin';
64
+ // Builtin files may be stamped one of two ways depending on when they were uploaded:
65
+ // legacy docs (pre-per-row-ids) used the builtin's config id (e.g. 'builtin') or have no
66
+ // storageConnectionId at all; current uploads use the row's own generated id (connId).
56
67
  const connMatch = isBuiltin
57
- ? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
68
+ ? { $or: [{ storageConnectionId: connId }, { storageConnectionId: conn.integrationId }, { storageConnectionId: { $exists: false } }] }
58
69
  : { storageConnectionId: connId };
59
70
  return { projectId, ...connMatch, ...(extra ?? {}) };
60
71
  }
61
72
  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'))) {
73
+ const conn = findStorageConn(project, connId);
74
+ if (!conn)
75
+ return { error: 'Storage connection not found', status: 404 };
76
+ if (conn.source === 'builtin') {
77
+ const builtinId = conn.integrationId;
78
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'storage'))) {
64
79
  return { error: 'Access to this built-in storage is not permitted', status: 403 };
65
80
  }
66
- const provider = config.storageProviders.find(p => p.id === connId);
81
+ const provider = config.storageProviders.find(p => p.id === builtinId);
67
82
  if (!provider)
68
83
  return { error: 'Storage provider not found', status: 404 };
69
84
  return provider.type === 's3' ? new s3_1.S3Adapter(provider.config) : adapter;
70
85
  }
71
- const conn = project.storageConnections?.find((c) => c.id === connId);
72
- if (!conn)
73
- return { error: 'Storage connection not found', status: 404 };
86
+ if (conn.source === 'integration') {
87
+ if (!conn.integrationId)
88
+ return { error: 'Connection is missing its integrationId', status: 500 };
89
+ try {
90
+ const s3Config = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
91
+ if (!s3Config)
92
+ return { error: 'Integration not found', status: 400 };
93
+ return new s3_1.S3Adapter(s3Config);
94
+ }
95
+ catch (e) {
96
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
97
+ }
98
+ }
74
99
  if (!(0, encryption_1.isEncryptionKeySet)())
75
100
  return { error: 'Encryption key not set', status: 500 };
76
101
  const credential = project.credentials?.find((c) => c.id === conn.credentialId);
@@ -354,7 +379,7 @@ function storageRouter(config) {
354
379
  const collection = getRawFilesCollection();
355
380
  if (!collection)
356
381
  return res.status(500).json({ message: 'Database not available' });
357
- const files = await collection.find(connFilter(projectId, connId)).sort({ createdAt: -1 }).limit(12).toArray();
382
+ const files = await collection.find(connFilter(projectId, connId, project)).sort({ createdAt: -1 }).limit(12).toArray();
358
383
  const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
359
384
  res.json({ data });
360
385
  }
@@ -380,7 +405,7 @@ function storageRouter(config) {
380
405
  const collection = getRawFilesCollection();
381
406
  if (!collection)
382
407
  return res.status(500).json({ message: 'Database not available' });
383
- const files = await collection.find(connFilter(projectId, connId, { category })).sort({ createdAt: -1 }).toArray();
408
+ const files = await collection.find(connFilter(projectId, connId, project, { category })).sort({ createdAt: -1 }).toArray();
384
409
  const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
385
410
  res.json({ data });
386
411
  }
@@ -404,7 +429,7 @@ function storageRouter(config) {
404
429
  const collection = getRawFilesCollection();
405
430
  if (!collection)
406
431
  return res.status(500).json({ message: 'Database not available' });
407
- const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
432
+ const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
408
433
  if (!fileDoc)
409
434
  return res.status(404).json({ message: 'File not found' });
410
435
  if (!validateContentType(contentType, fileName, fileDoc.category)) {
@@ -417,7 +442,7 @@ function storageRouter(config) {
417
442
  return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
418
443
  }
419
444
  const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType);
420
- await collection.updateOne(connFilter(projectId, connId, { fileId }), { $set: { contentType, updatedAt: new Date() } });
445
+ await collection.updateOne(connFilter(projectId, connId, project, { fileId }), { $set: { contentType, updatedAt: new Date() } });
421
446
  res.json({ url, storageKey: fileDoc.storageKey, fileId });
422
447
  }
423
448
  catch (err) {
@@ -440,7 +465,7 @@ function storageRouter(config) {
440
465
  const collection = getRawFilesCollection();
441
466
  if (!collection)
442
467
  return res.status(500).json({ message: 'Database not available' });
443
- const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
468
+ const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
444
469
  if (!result)
445
470
  return res.status(404).json({ message: 'File not found' });
446
471
  res.json({ message: 'File renamed successfully', file: result });
@@ -464,12 +489,12 @@ function storageRouter(config) {
464
489
  const collection = getRawFilesCollection();
465
490
  if (!collection)
466
491
  return res.status(500).json({ message: 'Database not available' });
467
- const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
492
+ const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
468
493
  if (!fileDoc)
469
494
  return res.status(404).json({ message: 'File not found' });
470
495
  if (connAdapter.deleteFile)
471
496
  await connAdapter.deleteFile(fileDoc.storageKey);
472
- await collection.deleteOne(connFilter(projectId, connId, { fileId }));
497
+ await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
473
498
  res.json({ message: 'File deleted successfully' });
474
499
  }
475
500
  catch (err) {
@@ -494,7 +519,7 @@ function storageRouter(config) {
494
519
  ? { parentId: null }
495
520
  : { parentId };
496
521
  const folders = await collection
497
- .find(connFilter(projectId, connId, parentFilter))
522
+ .find(connFilter(projectId, connId, project, parentFilter))
498
523
  .sort({ name: 1 })
499
524
  .toArray();
500
525
  res.json({
@@ -525,7 +550,7 @@ function storageRouter(config) {
525
550
  const collection = getRawFoldersCollection();
526
551
  if (!collection)
527
552
  return res.status(500).json({ message: 'Database not available' });
528
- const f = await collection.findOne(connFilter(projectId, connId, { folderId }));
553
+ const f = await collection.findOne(connFilter(projectId, connId, project, { folderId }));
529
554
  if (!f)
530
555
  return res.status(404).json({ message: 'Folder not found' });
531
556
  res.json({
@@ -557,8 +582,9 @@ function storageRouter(config) {
557
582
  return res.status(404).json({ message: 'Project not found' });
558
583
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
559
584
  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'))) {
585
+ const conn = findStorageConn(project, connId);
586
+ const isBuiltin = conn?.source === 'builtin';
587
+ if (isBuiltin && !(await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId, config, 'storage'))) {
562
588
  return res.status(403).json({ message: 'Access to this built-in storage is not permitted' });
563
589
  }
564
590
  const collection = getRawFoldersCollection();
@@ -610,7 +636,7 @@ function storageRouter(config) {
610
636
  const collection = getRawFoldersCollection();
611
637
  if (!collection)
612
638
  return res.status(500).json({ message: 'Database not available' });
613
- const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
639
+ const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
614
640
  if (!result)
615
641
  return res.status(404).json({ message: 'Folder not found' });
616
642
  res.json({
@@ -644,7 +670,7 @@ function storageRouter(config) {
644
670
  const collection = getRawFoldersCollection();
645
671
  if (!collection)
646
672
  return res.status(500).json({ message: 'Database not available' });
647
- const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
673
+ const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
648
674
  if (!result)
649
675
  return res.status(404).json({ message: 'Folder not found' });
650
676
  res.json({
@@ -674,15 +700,15 @@ function storageRouter(config) {
674
700
  const foldersCollection = getRawFoldersCollection();
675
701
  if (!foldersCollection)
676
702
  return res.status(500).json({ message: 'Database not available' });
677
- const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
703
+ const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
678
704
  if (!folderDoc)
679
705
  return res.status(404).json({ message: 'Folder not found' });
680
706
  const filesCollection = getRawFilesCollection();
681
707
  if (filesCollection) {
682
- await filesCollection.updateMany(connFilter(projectId, connId, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
708
+ await filesCollection.updateMany(connFilter(projectId, connId, project, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
683
709
  }
684
- await foldersCollection.updateMany(connFilter(projectId, connId, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
685
- await foldersCollection.deleteOne(connFilter(projectId, connId, { folderId }));
710
+ await foldersCollection.updateMany(connFilter(projectId, connId, project, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
711
+ await foldersCollection.deleteOne(connFilter(projectId, connId, project, { folderId }));
686
712
  res.json({ message: 'Folder deleted successfully' });
687
713
  }
688
714
  catch (err) {
@@ -702,7 +728,7 @@ function storageRouter(config) {
702
728
  if (folderId != null) {
703
729
  const foldersCollection = getRawFoldersCollection();
704
730
  if (foldersCollection) {
705
- const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
731
+ const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
706
732
  if (!folderDoc)
707
733
  return res.status(404).json({ message: 'Folder not found' });
708
734
  }
@@ -710,7 +736,7 @@ function storageRouter(config) {
710
736
  const filesCollection = getRawFilesCollection();
711
737
  if (!filesCollection)
712
738
  return res.status(500).json({ message: 'Database not available' });
713
- const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
739
+ const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
714
740
  if (!result)
715
741
  return res.status(404).json({ message: 'File not found' });
716
742
  res.json({ message: 'File moved successfully', file: result });
@@ -41,9 +41,15 @@ function storageConnectionRouter(config) {
41
41
  if (auth.role !== 'admin' && auth.role !== 'editor') {
42
42
  return res.status(403).json({ message: 'Permission denied' });
43
43
  }
44
- const { name, type, credentialId, description } = req.body;
45
- if (!name || !type || !credentialId) {
46
- return res.status(400).json({ message: 'name, type, and credentialId are required' });
44
+ const { name, type, credentialId, integrationId, description } = req.body;
45
+ if (!name || !type) {
46
+ return res.status(400).json({ message: 'name and type are required' });
47
+ }
48
+ if (!credentialId && !integrationId) {
49
+ return res.status(400).json({ message: 'Either credentialId or integrationId is required' });
50
+ }
51
+ if (credentialId && integrationId) {
52
+ return res.status(400).json({ message: 'Provide only one of credentialId or integrationId' });
47
53
  }
48
54
  if (!VALID_STORAGE_TYPES.includes(type)) {
49
55
  return res.status(400).json({ message: `type must be one of: ${VALID_STORAGE_TYPES.join(', ')}` });
@@ -53,16 +59,28 @@ function storageConnectionRouter(config) {
53
59
  return res.status(404).json({ message: 'Project not found' });
54
60
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
55
61
  return res.status(403).json({ message: 'Unauthorized project access' });
56
- const credExists = project.credentials?.some((c) => c.id === credentialId);
57
- if (!credExists)
58
- return res.status(400).json({ message: 'Credential not found in project' });
62
+ if (integrationId) {
63
+ const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
64
+ const integration = await UserIntegrationModel.findOne({ id: integrationId, userId: auth.userId });
65
+ if (!integration)
66
+ return res.status(404).json({ message: 'Integration not found' });
67
+ if (integration.kind !== 'storage')
68
+ return res.status(400).json({ message: 'Integration is not a storage integration' });
69
+ }
70
+ else {
71
+ const credExists = project.credentials?.some((c) => c.id === credentialId);
72
+ if (!credExists)
73
+ return res.status(400).json({ message: 'Credential not found in project' });
74
+ }
59
75
  const connection = {
60
76
  id: new mongodb_1.ObjectId().toHexString(),
61
77
  name,
62
78
  type,
63
- credentialId,
79
+ credentialId: credentialId ?? '',
64
80
  projectId: req.params.id,
65
81
  createdAt: new Date(),
82
+ source: integrationId ? 'integration' : 'manual',
83
+ ...(integrationId && { integrationId }),
66
84
  ...(description !== undefined && { description }),
67
85
  };
68
86
  await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
@@ -76,9 +94,6 @@ function storageConnectionRouter(config) {
76
94
  router.delete('/projects/:id/connections/storage/:connId', async (req, res) => {
77
95
  try {
78
96
  const { connId } = req.params;
79
- if ((0, builtinAccess_1.isBuiltinStorageId)(connId, config)) {
80
- return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
81
- }
82
97
  const auth = req.auth;
83
98
  if (auth.role !== 'admin' && auth.role !== 'editor') {
84
99
  return res.status(403).json({ message: 'Permission denied' });
@@ -88,9 +103,12 @@ function storageConnectionRouter(config) {
88
103
  return res.status(404).json({ message: 'Project not found' });
89
104
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
90
105
  return res.status(403).json({ message: 'Unauthorized project access' });
91
- const exists = project.storageConnections?.some((c) => c.id === connId);
92
- if (!exists)
106
+ const conn = project.storageConnections?.find((c) => c.id === connId);
107
+ if (!conn)
93
108
  return res.status(404).json({ message: 'Storage connection not found' });
109
+ if (conn.source === 'builtin') {
110
+ return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
111
+ }
94
112
  await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
95
113
  return res.status(200).json({ message: 'Storage connection deleted successfully' });
96
114
  }
@@ -22,11 +22,11 @@ function userIntegrationRouter(config) {
22
22
  // requiredPlan is not populated here — canUseBuiltin only returns a boolean today,
23
23
  // so there's no plan-level detail to surface without a hook-signature change.
24
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 });
25
+ pool.push({ id: db.id, kind: 'database', name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type, provider: db.type, source: 'builtin', available });
26
26
  }
27
27
  for (const provider of config.storageProviders ?? []) {
28
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 });
29
+ pool.push({ id: provider.id, kind: 'storage', name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type, provider: provider.type, source: 'builtin', available });
30
30
  }
31
31
  res.json({ data: pool });
32
32
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.backfillBuiltinConnections = backfillBuiltinConnections;
7
7
  const crypto_1 = __importDefault(require("crypto"));
8
8
  const manager_1 = require("../models/manager");
9
+ const builtinAccess_1 = require("../utils/builtinAccess");
9
10
  // Pre-BYOC, every project appeared connected to every configured builtin because
10
11
  // storageConnection.ts/dbConnection.ts SYNTHESIZED builtins at read time instead of
11
12
  // persisting them (see storageConnection.ts's old GET handler). Now that connections must
@@ -26,7 +27,7 @@ async function backfillBuiltinConnections(config) {
26
27
  const now = new Date();
27
28
  const dbConnections = (config.databases ?? []).map(db => ({
28
29
  id: crypto_1.default.randomUUID(),
29
- name: db.id,
30
+ name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type,
30
31
  dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
31
32
  credentialId: '',
32
33
  projectId: project.id,
@@ -37,7 +38,7 @@ async function backfillBuiltinConnections(config) {
37
38
  }));
38
39
  const storageConnections = (config.storageProviders ?? []).map(provider => ({
39
40
  id: crypto_1.default.randomUUID(),
40
- name: provider.id,
41
+ name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type,
41
42
  type: provider.type,
42
43
  credentialId: '',
43
44
  projectId: project.id,
@@ -0,0 +1,13 @@
1
+ import { Auth, DbConnection, StorageConnection, StreamByConfig } from '../types';
2
+ type ClassifyResult = {
3
+ kind: 'database';
4
+ connection: DbConnection;
5
+ } | {
6
+ kind: 'storage';
7
+ connection: StorageConnection;
8
+ } | {
9
+ error: string;
10
+ status: number;
11
+ };
12
+ export declare function classifyIntegrationId(id: string, auth: Auth, config: StreamByConfig, projectId: string): Promise<ClassifyResult>;
13
+ export {};
@@ -0,0 +1,63 @@
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.classifyIntegrationId = classifyIntegrationId;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const manager_1 = require("../models/manager");
9
+ const builtinAccess_1 = require("../utils/builtinAccess");
10
+ // Classifies a client-supplied integrationId (builtin db, builtin storage, or a user's own
11
+ // BYOC integration) and revalidates access/ownership server-side — never trust the client.
12
+ // Shared by POST /projects/create (bulk, at project creation) and
13
+ // POST /projects/:id/integrations (single, added to an existing project).
14
+ async function classifyIntegrationId(id, auth, config, projectId) {
15
+ const now = new Date();
16
+ if ((0, builtinAccess_1.isBuiltinDb)(id, config)) {
17
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'database'))) {
18
+ return { error: `Access to built-in database '${id}' is not permitted`, status: 403 };
19
+ }
20
+ const db = config.databases.find(d => d.id === id);
21
+ return {
22
+ kind: 'database',
23
+ connection: {
24
+ id: crypto_1.default.randomUUID(), name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type, dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
25
+ credentialId: '', projectId, createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
26
+ },
27
+ };
28
+ }
29
+ if ((0, builtinAccess_1.isBuiltinStorageId)(id, config)) {
30
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'storage'))) {
31
+ return { error: `Access to built-in storage '${id}' is not permitted`, status: 403 };
32
+ }
33
+ const provider = config.storageProviders.find(p => p.id === id);
34
+ return {
35
+ kind: 'storage',
36
+ connection: {
37
+ id: crypto_1.default.randomUUID(), name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type, type: provider.type,
38
+ credentialId: '', projectId, createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
39
+ },
40
+ };
41
+ }
42
+ const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
43
+ const integration = await UserIntegrationModel.findOne({ id, userId: auth.userId });
44
+ if (!integration) {
45
+ return { error: `Integration '${id}' is not valid or does not belong to you`, status: 403 };
46
+ }
47
+ if (integration.kind === 'database') {
48
+ return {
49
+ kind: 'database',
50
+ connection: {
51
+ id: crypto_1.default.randomUUID(), name: integration.name, dbType: integration.provider,
52
+ credentialId: '', projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
53
+ },
54
+ };
55
+ }
56
+ return {
57
+ kind: 'storage',
58
+ connection: {
59
+ id: crypto_1.default.randomUUID(), name: integration.name, type: integration.provider,
60
+ credentialId: '', projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
61
+ },
62
+ };
63
+ }
@@ -14,3 +14,4 @@ export declare function updateUserIntegration(userId: string, integrationId: str
14
14
  }): Promise<UserIntegration | null>;
15
15
  export declare function deleteUserIntegration(userId: string, integrationId: string): Promise<boolean>;
16
16
  export declare function getDecryptedIntegrationCredential(userId: string, integrationId: string): Promise<unknown | null>;
17
+ export declare function getDecryptedIntegrationCredentialById(integrationId: string): Promise<unknown | null>;
@@ -8,6 +8,7 @@ exports.createUserIntegration = createUserIntegration;
8
8
  exports.updateUserIntegration = updateUserIntegration;
9
9
  exports.deleteUserIntegration = deleteUserIntegration;
10
10
  exports.getDecryptedIntegrationCredential = getDecryptedIntegrationCredential;
11
+ exports.getDecryptedIntegrationCredentialById = getDecryptedIntegrationCredentialById;
11
12
  const crypto_1 = __importDefault(require("crypto"));
12
13
  const manager_1 = require("../models/manager");
13
14
  const encryption_1 = require("../utils/encryption");
@@ -63,6 +64,18 @@ async function deleteUserIntegration(userId, integrationId) {
63
64
  async function getDecryptedIntegrationCredential(userId, integrationId) {
64
65
  const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
65
66
  const integration = await UserIntegrationModel.findOne({ id: integrationId, userId });
67
+ return decryptIntegration(integration);
68
+ }
69
+ // Used when resolving a project connection's `integrationId` at connection-use time — the
70
+ // project already validated ownership when the connection was created (see project.ts /
71
+ // dbConnection.ts POST), so there's no userId to filter by here, only the integrationId
72
+ // stored on the connection itself.
73
+ async function getDecryptedIntegrationCredentialById(integrationId) {
74
+ const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
75
+ const integration = await UserIntegrationModel.findOne({ id: integrationId });
76
+ return decryptIntegration(integration);
77
+ }
78
+ async function decryptIntegration(integration) {
66
79
  if (!integration)
67
80
  return null;
68
81
  if (!(0, encryption_1.isEncryptionKeySet)()) {
@@ -18,7 +18,7 @@ export interface StorageConnection {
18
18
  description?: string;
19
19
  isBuiltin?: boolean;
20
20
  integrationId?: string;
21
- source?: 'builtin' | 'integration';
21
+ source?: 'builtin' | 'integration' | 'manual';
22
22
  available?: boolean;
23
23
  }
24
24
  export type StorageProvider = {
@@ -159,7 +159,7 @@ export interface DbConnection {
159
159
  description?: string;
160
160
  isBuiltin?: boolean;
161
161
  integrationId?: string;
162
- source?: 'builtin' | 'integration';
162
+ source?: 'builtin' | 'integration' | 'manual';
163
163
  }
164
164
  export type IntegrationKind = 'database' | 'storage';
165
165
  export interface UserIntegration {
@@ -2,3 +2,5 @@ import { Auth, StreamByConfig } from '../types';
2
2
  export declare function assertBuiltinAccess(auth: Auth, builtinId: string, config: StreamByConfig, kind: 'database' | 'storage'): Promise<boolean>;
3
3
  export declare function isBuiltinStorageId(id: string, config: StreamByConfig): boolean;
4
4
  export declare function isBuiltinDb(id: string, config: StreamByConfig): boolean;
5
+ export declare const BUILTIN_STORAGE_DISPLAY: Record<string, string>;
6
+ export declare const BUILTIN_DB_DISPLAY: Record<string, string>;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILTIN_DB_DISPLAY = exports.BUILTIN_STORAGE_DISPLAY = void 0;
3
4
  exports.assertBuiltinAccess = assertBuiltinAccess;
4
5
  exports.isBuiltinStorageId = isBuiltinStorageId;
5
6
  exports.isBuiltinDb = isBuiltinDb;
@@ -17,3 +18,15 @@ function isBuiltinStorageId(id, config) {
17
18
  function isBuiltinDb(id, config) {
18
19
  return (config.databases ?? []).some(db => db.id === id);
19
20
  }
21
+ // Display names for builtin connections — never the raw config id/type. The frontend pairs
22
+ // this with the StreamBy icon to visually mark a connection as built-in vs BYOC.
23
+ exports.BUILTIN_STORAGE_DISPLAY = {
24
+ s3: 'AWS S3',
25
+ gcs: 'Google Cloud Storage',
26
+ r2: 'Cloudflare R2',
27
+ azure: 'Azure Blob Storage',
28
+ };
29
+ exports.BUILTIN_DB_DISPLAY = {
30
+ sql: 'PostgreSQL',
31
+ nosql: 'MongoDB',
32
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamby-core",
3
- "version": "0.29.9",
3
+ "version": "0.29.12",
4
4
  "description": "StreamBy middleware framework for media storage management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",