streamby-core 0.29.8 → 0.29.11

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.
@@ -13,6 +13,8 @@ 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");
17
+ const userIntegration_1 = require("../../services/userIntegration");
16
18
  const mongodb_1 = require("mongodb");
17
19
  const crypto_1 = __importDefault(require("crypto"));
18
20
  const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
@@ -46,36 +48,64 @@ function getRawFoldersCollection() {
46
48
  return null;
47
49
  return (0, connectionManager_1.getConnection)(activeId).client.db().collection('storage_folders');
48
50
  }
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
51
  function storageRouter(config) {
76
52
  const router = (0, express_1.Router)();
77
53
  const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
78
54
  const Project = (0, manager_1.getModel)('projects');
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
+ const connMatch = isBuiltin
65
+ ? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
66
+ : { storageConnectionId: connId };
67
+ return { projectId, ...connMatch, ...(extra ?? {}) };
68
+ }
69
+ async function resolveConnAdapter(connId, project, auth) {
70
+ const conn = findStorageConn(project, connId);
71
+ if (!conn)
72
+ return { error: 'Storage connection not found', status: 404 };
73
+ if (conn.source === 'builtin') {
74
+ const builtinId = conn.integrationId;
75
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'storage'))) {
76
+ return { error: 'Access to this built-in storage is not permitted', status: 403 };
77
+ }
78
+ const provider = config.storageProviders.find(p => p.id === builtinId);
79
+ if (!provider)
80
+ return { error: 'Storage provider not found', status: 404 };
81
+ return provider.type === 's3' ? new s3_1.S3Adapter(provider.config) : adapter;
82
+ }
83
+ if (conn.source === 'integration') {
84
+ if (!conn.integrationId)
85
+ return { error: 'Connection is missing its integrationId', status: 500 };
86
+ try {
87
+ const s3Config = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
88
+ if (!s3Config)
89
+ return { error: 'Integration not found', status: 400 };
90
+ return new s3_1.S3Adapter(s3Config);
91
+ }
92
+ catch (e) {
93
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
94
+ }
95
+ }
96
+ if (!(0, encryption_1.isEncryptionKeySet)())
97
+ return { error: 'Encryption key not set', status: 500 };
98
+ const credential = project.credentials?.find((c) => c.id === conn.credentialId);
99
+ if (!credential)
100
+ return { error: 'Credential not found in project', status: 400 };
101
+ try {
102
+ const s3Config = JSON.parse((0, encryption_1.decrypt)(credential.encryptedValue));
103
+ return new s3_1.S3Adapter(s3Config);
104
+ }
105
+ catch (e) {
106
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
107
+ }
108
+ }
79
109
  router.get('/upload-project-image-url/:id', async (req, res) => {
80
110
  try {
81
111
  const auth = req.auth;
@@ -301,7 +331,7 @@ function storageRouter(config) {
301
331
  return res.status(404).json({ message: 'Project not found' });
302
332
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
303
333
  return res.status(403).json({ message: 'Unauthorized access' });
304
- const connAdapter = await resolveConnAdapter(connId, project, adapter);
334
+ const connAdapter = await resolveConnAdapter(connId, project, auth);
305
335
  if ('error' in connAdapter)
306
336
  return res.status(connAdapter.status).json({ message: connAdapter.error });
307
337
  if (!connAdapter.getPresignedUploadUrl)
@@ -340,13 +370,13 @@ function storageRouter(config) {
340
370
  return res.status(404).json({ message: 'Project not found' });
341
371
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
342
372
  return res.status(403).json({ message: 'Unauthorized access' });
343
- const connAdapter = await resolveConnAdapter(connId, project, adapter);
373
+ const connAdapter = await resolveConnAdapter(connId, project, auth);
344
374
  if ('error' in connAdapter)
345
375
  return res.status(connAdapter.status).json({ message: connAdapter.error });
346
376
  const collection = getRawFilesCollection();
347
377
  if (!collection)
348
378
  return res.status(500).json({ message: 'Database not available' });
349
- const files = await collection.find(connFilter(projectId, connId)).sort({ createdAt: -1 }).limit(12).toArray();
379
+ const files = await collection.find(connFilter(projectId, connId, project)).sort({ createdAt: -1 }).limit(12).toArray();
350
380
  const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
351
381
  res.json({ data });
352
382
  }
@@ -366,13 +396,13 @@ function storageRouter(config) {
366
396
  return res.status(404).json({ message: 'Project not found' });
367
397
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
368
398
  return res.status(403).json({ message: 'Unauthorized access' });
369
- const connAdapter = await resolveConnAdapter(connId, project, adapter);
399
+ const connAdapter = await resolveConnAdapter(connId, project, auth);
370
400
  if ('error' in connAdapter)
371
401
  return res.status(connAdapter.status).json({ message: connAdapter.error });
372
402
  const collection = getRawFilesCollection();
373
403
  if (!collection)
374
404
  return res.status(500).json({ message: 'Database not available' });
375
- const files = await collection.find(connFilter(projectId, connId, { category })).sort({ createdAt: -1 }).toArray();
405
+ const files = await collection.find(connFilter(projectId, connId, project, { category })).sort({ createdAt: -1 }).toArray();
376
406
  const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
377
407
  res.json({ data });
378
408
  }
@@ -396,20 +426,20 @@ function storageRouter(config) {
396
426
  const collection = getRawFilesCollection();
397
427
  if (!collection)
398
428
  return res.status(500).json({ message: 'Database not available' });
399
- const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
429
+ const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
400
430
  if (!fileDoc)
401
431
  return res.status(404).json({ message: 'File not found' });
402
432
  if (!validateContentType(contentType, fileName, fileDoc.category)) {
403
433
  return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
404
434
  }
405
- const connAdapter = await resolveConnAdapter(connId, project, adapter);
435
+ const connAdapter = await resolveConnAdapter(connId, project, auth);
406
436
  if ('error' in connAdapter)
407
437
  return res.status(connAdapter.status).json({ message: connAdapter.error });
408
438
  if (!connAdapter.getPresignedUploadUrl) {
409
439
  return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
410
440
  }
411
441
  const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType);
412
- await collection.updateOne(connFilter(projectId, connId, { fileId }), { $set: { contentType, updatedAt: new Date() } });
442
+ await collection.updateOne(connFilter(projectId, connId, project, { fileId }), { $set: { contentType, updatedAt: new Date() } });
413
443
  res.json({ url, storageKey: fileDoc.storageKey, fileId });
414
444
  }
415
445
  catch (err) {
@@ -432,7 +462,7 @@ function storageRouter(config) {
432
462
  const collection = getRawFilesCollection();
433
463
  if (!collection)
434
464
  return res.status(500).json({ message: 'Database not available' });
435
- const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
465
+ const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
436
466
  if (!result)
437
467
  return res.status(404).json({ message: 'File not found' });
438
468
  res.json({ message: 'File renamed successfully', file: result });
@@ -450,18 +480,18 @@ function storageRouter(config) {
450
480
  return res.status(404).json({ message: 'Project not found' });
451
481
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
452
482
  return res.status(403).json({ message: 'Unauthorized access' });
453
- const connAdapter = await resolveConnAdapter(connId, project, adapter);
483
+ const connAdapter = await resolveConnAdapter(connId, project, auth);
454
484
  if ('error' in connAdapter)
455
485
  return res.status(connAdapter.status).json({ message: connAdapter.error });
456
486
  const collection = getRawFilesCollection();
457
487
  if (!collection)
458
488
  return res.status(500).json({ message: 'Database not available' });
459
- const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
489
+ const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
460
490
  if (!fileDoc)
461
491
  return res.status(404).json({ message: 'File not found' });
462
492
  if (connAdapter.deleteFile)
463
493
  await connAdapter.deleteFile(fileDoc.storageKey);
464
- await collection.deleteOne(connFilter(projectId, connId, { fileId }));
494
+ await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
465
495
  res.json({ message: 'File deleted successfully' });
466
496
  }
467
497
  catch (err) {
@@ -486,7 +516,7 @@ function storageRouter(config) {
486
516
  ? { parentId: null }
487
517
  : { parentId };
488
518
  const folders = await collection
489
- .find(connFilter(projectId, connId, parentFilter))
519
+ .find(connFilter(projectId, connId, project, parentFilter))
490
520
  .sort({ name: 1 })
491
521
  .toArray();
492
522
  res.json({
@@ -517,7 +547,7 @@ function storageRouter(config) {
517
547
  const collection = getRawFoldersCollection();
518
548
  if (!collection)
519
549
  return res.status(500).json({ message: 'Database not available' });
520
- const f = await collection.findOne(connFilter(projectId, connId, { folderId }));
550
+ const f = await collection.findOne(connFilter(projectId, connId, project, { folderId }));
521
551
  if (!f)
522
552
  return res.status(404).json({ message: 'Folder not found' });
523
553
  res.json({
@@ -549,11 +579,15 @@ function storageRouter(config) {
549
579
  return res.status(404).json({ message: 'Project not found' });
550
580
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
551
581
  return res.status(403).json({ message: 'Unauthorized access' });
582
+ const conn = findStorageConn(project, connId);
583
+ const isBuiltin = conn?.source === 'builtin';
584
+ if (isBuiltin && !(await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId, config, 'storage'))) {
585
+ return res.status(403).json({ message: 'Access to this built-in storage is not permitted' });
586
+ }
552
587
  const collection = getRawFoldersCollection();
553
588
  if (!collection)
554
589
  return res.status(500).json({ message: 'Database not available' });
555
590
  const folderId = crypto_1.default.randomUUID();
556
- const isBuiltin = connId === 'builtin' || connId.startsWith('builtin-');
557
591
  const folderDoc = {
558
592
  _id: new mongodb_1.ObjectId(),
559
593
  folderId,
@@ -599,7 +633,7 @@ function storageRouter(config) {
599
633
  const collection = getRawFoldersCollection();
600
634
  if (!collection)
601
635
  return res.status(500).json({ message: 'Database not available' });
602
- const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
636
+ const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
603
637
  if (!result)
604
638
  return res.status(404).json({ message: 'Folder not found' });
605
639
  res.json({
@@ -633,7 +667,7 @@ function storageRouter(config) {
633
667
  const collection = getRawFoldersCollection();
634
668
  if (!collection)
635
669
  return res.status(500).json({ message: 'Database not available' });
636
- const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
670
+ const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
637
671
  if (!result)
638
672
  return res.status(404).json({ message: 'Folder not found' });
639
673
  res.json({
@@ -663,15 +697,15 @@ function storageRouter(config) {
663
697
  const foldersCollection = getRawFoldersCollection();
664
698
  if (!foldersCollection)
665
699
  return res.status(500).json({ message: 'Database not available' });
666
- const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
700
+ const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
667
701
  if (!folderDoc)
668
702
  return res.status(404).json({ message: 'Folder not found' });
669
703
  const filesCollection = getRawFilesCollection();
670
704
  if (filesCollection) {
671
- await filesCollection.updateMany(connFilter(projectId, connId, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
705
+ await filesCollection.updateMany(connFilter(projectId, connId, project, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
672
706
  }
673
- await foldersCollection.updateMany(connFilter(projectId, connId, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
674
- await foldersCollection.deleteOne(connFilter(projectId, connId, { folderId }));
707
+ await foldersCollection.updateMany(connFilter(projectId, connId, project, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
708
+ await foldersCollection.deleteOne(connFilter(projectId, connId, project, { folderId }));
675
709
  res.json({ message: 'Folder deleted successfully' });
676
710
  }
677
711
  catch (err) {
@@ -691,7 +725,7 @@ function storageRouter(config) {
691
725
  if (folderId != null) {
692
726
  const foldersCollection = getRawFoldersCollection();
693
727
  if (foldersCollection) {
694
- const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
728
+ const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
695
729
  if (!folderDoc)
696
730
  return res.status(404).json({ message: 'Folder not found' });
697
731
  }
@@ -699,7 +733,7 @@ function storageRouter(config) {
699
733
  const filesCollection = getRawFilesCollection();
700
734
  if (!filesCollection)
701
735
  return res.status(500).json({ message: 'Database not available' });
702
- const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
736
+ const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
703
737
  if (!result)
704
738
  return res.status(404).json({ message: 'File not found' });
705
739
  res.json({ message: 'File moved successfully', file: result });
@@ -717,6 +751,7 @@ function storageRouter(config) {
717
751
  r2: { value: 'cloudflare-r2', name: 'Cloudflare R2' },
718
752
  };
719
753
  const storages = (config.storageProviders || []).map(provider => ({
754
+ id: provider.id,
720
755
  ...STORAGE_DISPLAY[provider.type],
721
756
  type: provider.type,
722
757
  }));
@@ -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 builtinConnections = (config.storageProviders || []).map((provider, i) => ({
28
- id: i === 0 ? 'builtin' : `builtin-${i}`,
29
- name: STORAGE_DISPLAY[provider.type] || provider.type,
30
- type: provider.type,
31
- credentialId: '',
32
- projectId: req.params.id,
33
- createdAt: new Date(0),
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
- const projectConnections = project.storageConnections || [];
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 });
@@ -47,9 +41,15 @@ function storageConnectionRouter(config) {
47
41
  if (auth.role !== 'admin' && auth.role !== 'editor') {
48
42
  return res.status(403).json({ message: 'Permission denied' });
49
43
  }
50
- const { name, type, credentialId, description } = req.body;
51
- if (!name || !type || !credentialId) {
52
- 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' });
53
53
  }
54
54
  if (!VALID_STORAGE_TYPES.includes(type)) {
55
55
  return res.status(400).json({ message: `type must be one of: ${VALID_STORAGE_TYPES.join(', ')}` });
@@ -59,16 +59,28 @@ function storageConnectionRouter(config) {
59
59
  return res.status(404).json({ message: 'Project not found' });
60
60
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
61
61
  return res.status(403).json({ message: 'Unauthorized project access' });
62
- const credExists = project.credentials?.some((c) => c.id === credentialId);
63
- if (!credExists)
64
- 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
+ }
65
75
  const connection = {
66
76
  id: new mongodb_1.ObjectId().toHexString(),
67
77
  name,
68
78
  type,
69
- credentialId,
79
+ credentialId: credentialId ?? '',
70
80
  projectId: req.params.id,
71
81
  createdAt: new Date(),
82
+ source: integrationId ? 'integration' : 'manual',
83
+ ...(integrationId && { integrationId }),
72
84
  ...(description !== undefined && { description }),
73
85
  };
74
86
  await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
@@ -82,9 +94,6 @@ function storageConnectionRouter(config) {
82
94
  router.delete('/projects/:id/connections/storage/:connId', async (req, res) => {
83
95
  try {
84
96
  const { connId } = req.params;
85
- if (connId === 'builtin' || connId.startsWith('builtin-')) {
86
- return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
87
- }
88
97
  const auth = req.auth;
89
98
  if (auth.role !== 'admin' && auth.role !== 'editor') {
90
99
  return res.status(403).json({ message: 'Permission denied' });
@@ -94,9 +103,12 @@ function storageConnectionRouter(config) {
94
103
  return res.status(404).json({ message: 'Project not found' });
95
104
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
96
105
  return res.status(403).json({ message: 'Unauthorized project access' });
97
- const exists = project.storageConnections?.some((c) => c.id === connId);
98
- if (!exists)
106
+ const conn = project.storageConnections?.find((c) => c.id === connId);
107
+ if (!conn)
99
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
+ }
100
112
  await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
101
113
  return res.status(200).json({ message: 'Storage connection deleted successfully' });
102
114
  }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function userIntegrationRouter(config: StreamByConfig): Router;
@@ -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: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type, 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: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type, 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,2 @@
1
+ import { StreamByConfig } from '../types';
2
+ export declare function backfillBuiltinConnections(config: StreamByConfig): Promise<void>;
@@ -0,0 +1,57 @@
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
+ const builtinAccess_1 = require("../utils/builtinAccess");
10
+ // Pre-BYOC, every project appeared connected to every configured builtin because
11
+ // storageConnection.ts/dbConnection.ts SYNTHESIZED builtins at read time instead of
12
+ // persisting them (see storageConnection.ts's old GET handler). Now that connections must
13
+ // be real rows on the project, this seeds those rows once for projects that predate the
14
+ // change — so existing projects don't silently lose access on deploy.
15
+ //
16
+ // Guarded by ProjectInfo.builtinBackfilledAt: runs once per project, ever. An admin who
17
+ // later disconnects a builtin on purpose won't have it silently reappear on the next boot.
18
+ async function backfillBuiltinConnections(config) {
19
+ if (!config.databases?.length && !config.storageProviders?.length)
20
+ return;
21
+ const Project = (0, manager_1.getModel)('projects');
22
+ const allProjects = await Project.find({});
23
+ const pending = allProjects.filter(p => !p.builtinBackfilledAt);
24
+ if (!pending.length)
25
+ return;
26
+ for (const project of pending) {
27
+ const now = new Date();
28
+ const dbConnections = (config.databases ?? []).map(db => ({
29
+ id: crypto_1.default.randomUUID(),
30
+ name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type,
31
+ dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
32
+ credentialId: '',
33
+ projectId: project.id,
34
+ createdAt: now,
35
+ isBuiltin: true,
36
+ integrationId: db.id,
37
+ source: 'builtin',
38
+ }));
39
+ const storageConnections = (config.storageProviders ?? []).map(provider => ({
40
+ id: crypto_1.default.randomUUID(),
41
+ name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type,
42
+ type: provider.type,
43
+ credentialId: '',
44
+ projectId: project.id,
45
+ createdAt: now,
46
+ isBuiltin: true,
47
+ integrationId: provider.id,
48
+ source: 'builtin',
49
+ }));
50
+ await Project.update({ _id: project.id }, {
51
+ dbConnections: [...(project.dbConnections ?? []), ...dbConnections],
52
+ storageConnections: [...(project.storageConnections ?? []), ...storageConnections],
53
+ builtinBackfilledAt: now,
54
+ });
55
+ }
56
+ console.log(`✅ Backfilled built-in connections for ${pending.length} project(s).`);
57
+ }
@@ -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
+ }