streamby-core 0.29.12 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,8 @@ const mongodb_1 = require("mongodb");
6
6
  const manager_1 = require("../../models/manager");
7
7
  const auth_1 = require("../../utils/auth");
8
8
  const builtinAccess_1 = require("../../utils/builtinAccess");
9
+ const sanitize_1 = require("../../utils/sanitize");
10
+ const encryption_1 = require("../../utils/encryption");
9
11
  const VALID_STORAGE_TYPES = ['s3', 'gcs', 'r2', 'azure'];
10
12
  function storageConnectionRouter(config) {
11
13
  const router = (0, express_1.Router)();
@@ -24,9 +26,9 @@ function storageConnectionRouter(config) {
24
26
  // "validate on every use" (TCORE-69). BYOC (source: 'integration') is always available.
25
27
  const data = await Promise.all(connections.map(async (conn) => {
26
28
  if (conn.source !== 'builtin')
27
- return conn;
29
+ return (0, sanitize_1.sanitizeConnection)(conn);
28
30
  const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
29
- return { ...conn, isBuiltin: true, available };
31
+ return (0, sanitize_1.sanitizeConnection)({ ...conn, isBuiltin: true, available });
30
32
  }));
31
33
  return res.status(200).json({ data });
32
34
  }
@@ -41,15 +43,15 @@ function storageConnectionRouter(config) {
41
43
  if (auth.role !== 'admin' && auth.role !== 'editor') {
42
44
  return res.status(403).json({ message: 'Permission denied' });
43
45
  }
44
- const { name, type, credentialId, integrationId, description } = req.body;
46
+ const { name, type, credential, integrationId, description } = req.body;
45
47
  if (!name || !type) {
46
48
  return res.status(400).json({ message: 'name and type are required' });
47
49
  }
48
- if (!credentialId && !integrationId) {
49
- return res.status(400).json({ message: 'Either credentialId or integrationId is required' });
50
+ if (!credential && !integrationId) {
51
+ return res.status(400).json({ message: 'Either credential or integrationId is required' });
50
52
  }
51
- if (credentialId && integrationId) {
52
- return res.status(400).json({ message: 'Provide only one of credentialId or integrationId' });
53
+ if (credential && integrationId) {
54
+ return res.status(400).json({ message: 'Provide only one of credential or integrationId' });
53
55
  }
54
56
  if (!VALID_STORAGE_TYPES.includes(type)) {
55
57
  return res.status(400).json({ message: `type must be one of: ${VALID_STORAGE_TYPES.join(', ')}` });
@@ -59,6 +61,7 @@ function storageConnectionRouter(config) {
59
61
  return res.status(404).json({ message: 'Project not found' });
60
62
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
61
63
  return res.status(403).json({ message: 'Unauthorized project access' });
64
+ let encryptedCredential;
62
65
  if (integrationId) {
63
66
  const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
64
67
  const integration = await UserIntegrationModel.findOne({ id: integrationId, userId: auth.userId });
@@ -68,23 +71,23 @@ function storageConnectionRouter(config) {
68
71
  return res.status(400).json({ message: 'Integration is not a storage integration' });
69
72
  }
70
73
  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
+ if (!(0, encryption_1.isEncryptionKeySet)())
75
+ return res.status(500).json({ message: 'Encryption key is not set' });
76
+ encryptedCredential = (0, encryption_1.encrypt)(credential);
74
77
  }
75
78
  const connection = {
76
79
  id: new mongodb_1.ObjectId().toHexString(),
77
80
  name,
78
81
  type,
79
- credentialId: credentialId ?? '',
80
82
  projectId: req.params.id,
81
83
  createdAt: new Date(),
82
84
  source: integrationId ? 'integration' : 'manual',
83
85
  ...(integrationId && { integrationId }),
86
+ ...(encryptedCredential !== undefined && { encryptedCredential }),
84
87
  ...(description !== undefined && { description }),
85
88
  };
86
89
  await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
87
- return res.status(201).json({ data: connection });
90
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection) });
88
91
  }
89
92
  catch (err) {
90
93
  res.status(500).json({ message: 'Failed to add storage connection', details: err.message });
@@ -29,7 +29,6 @@ async function backfillBuiltinConnections(config) {
29
29
  id: crypto_1.default.randomUUID(),
30
30
  name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type,
31
31
  dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
32
- credentialId: '',
33
32
  projectId: project.id,
34
33
  createdAt: now,
35
34
  isBuiltin: true,
@@ -40,7 +39,6 @@ async function backfillBuiltinConnections(config) {
40
39
  id: crypto_1.default.randomUUID(),
41
40
  name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type,
42
41
  type: provider.type,
43
- credentialId: '',
44
42
  projectId: project.id,
45
43
  createdAt: now,
46
44
  isBuiltin: true,
@@ -0,0 +1,18 @@
1
+ import { StreamByConfig, Connection, ConnectionMethod } from '../types';
2
+ export declare function addConnection(config: StreamByConfig, projectId: string, data: {
3
+ name: string;
4
+ apiUrl: string;
5
+ method: ConnectionMethod;
6
+ prefix?: string;
7
+ description?: string;
8
+ token?: string;
9
+ }): Promise<Connection>;
10
+ export declare function updateConnection(config: StreamByConfig, projectId: string, connectionId: string, data: {
11
+ name?: string;
12
+ apiUrl?: string;
13
+ method?: ConnectionMethod;
14
+ prefix?: string;
15
+ description?: string;
16
+ token?: string;
17
+ }): Promise<Connection>;
18
+ export declare function deleteConnection(config: StreamByConfig, projectId: string, connectionId: string): Promise<void>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addConnection = addConnection;
4
+ exports.updateConnection = updateConnection;
5
+ exports.deleteConnection = deleteConnection;
6
+ const mongodb_1 = require("mongodb");
7
+ const manager_1 = require("../models/manager");
8
+ const encryption_1 = require("../utils/encryption");
9
+ async function addConnection(config, projectId, data) {
10
+ const ProjectModel = (0, manager_1.getModel)('projects');
11
+ const project = await ProjectModel.findOne({ _id: projectId });
12
+ if (!project) {
13
+ throw new Error('Project not found.');
14
+ }
15
+ let encryptedCredential;
16
+ if (data.token) {
17
+ if (!(0, encryption_1.isEncryptionKeySet)())
18
+ throw new Error('Encryption key is not set. Cannot encrypt credential.');
19
+ encryptedCredential = (0, encryption_1.encrypt)(data.token);
20
+ }
21
+ const connection = {
22
+ id: new mongodb_1.ObjectId().toHexString(),
23
+ name: data.name,
24
+ prefix: data.prefix,
25
+ apiUrl: data.apiUrl,
26
+ method: data.method,
27
+ projectId,
28
+ createdAt: new Date(),
29
+ ...(data.description !== undefined && { description: data.description }),
30
+ ...(encryptedCredential !== undefined && { encryptedCredential }),
31
+ };
32
+ const updatedProject = await ProjectModel.update({ _id: projectId }, { $push: { connections: connection } });
33
+ if (!updatedProject) {
34
+ throw new Error('Failed to add connection to project.');
35
+ }
36
+ return connection;
37
+ }
38
+ async function updateConnection(config, projectId, connectionId, data) {
39
+ const ProjectModel = (0, manager_1.getModel)('projects');
40
+ const project = await ProjectModel.findOne({ _id: projectId });
41
+ if (!project)
42
+ throw new Error('Project not found.');
43
+ const connections = project.connections ?? [];
44
+ const index = connections.findIndex((c) => c.id === connectionId);
45
+ if (index === -1)
46
+ throw new Error('Connection not found.');
47
+ let encryptedCredential;
48
+ if (data.token) {
49
+ if (!(0, encryption_1.isEncryptionKeySet)())
50
+ throw new Error('Encryption key is not set. Cannot encrypt credential.');
51
+ encryptedCredential = (0, encryption_1.encrypt)(data.token);
52
+ }
53
+ const updated = {
54
+ ...connections[index],
55
+ ...(data.name !== undefined && { name: data.name }),
56
+ ...(data.apiUrl !== undefined && { apiUrl: data.apiUrl }),
57
+ ...(data.method !== undefined && { method: data.method }),
58
+ ...(data.prefix !== undefined && { prefix: data.prefix }),
59
+ ...(data.description !== undefined && { description: data.description }),
60
+ ...(encryptedCredential !== undefined && { encryptedCredential }),
61
+ };
62
+ connections[index] = updated;
63
+ const result = await ProjectModel.update({ _id: projectId }, { $set: { connections } });
64
+ if (!result)
65
+ throw new Error('Failed to update connection.');
66
+ return updated;
67
+ }
68
+ async function deleteConnection(config, projectId, connectionId) {
69
+ const ProjectModel = (0, manager_1.getModel)('projects');
70
+ const project = await ProjectModel.findOne({ _id: projectId });
71
+ if (!project) {
72
+ throw new Error('Project not found.');
73
+ }
74
+ const updatedProject = await ProjectModel.update({ _id: projectId }, { $pull: { connections: { id: connectionId } } });
75
+ if (!updatedProject) {
76
+ throw new Error('Failed to delete connection from project.');
77
+ }
78
+ }
@@ -0,0 +1,19 @@
1
+ import { Pool } from 'pg';
2
+ import { MongoClient } from 'mongodb';
3
+ import { StreamByConfig, Auth, DbConnection, StorageConnection, StorageAdapter } from '../types';
4
+ export type ResolvedDbClient = {
5
+ client: Pool | MongoClient;
6
+ type: 'sql' | 'nosql';
7
+ ephemeral: boolean;
8
+ };
9
+ export type ResolveError = {
10
+ error: string;
11
+ status: number;
12
+ };
13
+ export declare function findDbConnection(project: any, connId: string): DbConnection | undefined;
14
+ export declare function findStorageConnection(project: any, connId: string): StorageConnection | undefined;
15
+ export declare function resolveDbConnectionClient(project: any, connId: string, config: StreamByConfig, auth: Auth): Promise<ResolvedDbClient | ResolveError>;
16
+ export declare function releaseDbClient(resolved: ResolvedDbClient): Promise<void>;
17
+ export declare function resolveStorageAdapter(project: any, connId: string, config: StreamByConfig & {
18
+ adapter?: StorageAdapter;
19
+ }, auth: Auth): Promise<StorageAdapter | ResolveError>;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findDbConnection = findDbConnection;
4
+ exports.findStorageConnection = findStorageConnection;
5
+ exports.resolveDbConnectionClient = resolveDbConnectionClient;
6
+ exports.releaseDbClient = releaseDbClient;
7
+ exports.resolveStorageAdapter = resolveStorageAdapter;
8
+ const pg_1 = require("pg");
9
+ const mongodb_1 = require("mongodb");
10
+ const connectionManager_1 = require("../adapters/database/connectionManager");
11
+ const builtinAccess_1 = require("../utils/builtinAccess");
12
+ const userIntegration_1 = require("./userIntegration");
13
+ const encryption_1 = require("../utils/encryption");
14
+ const storage_1 = require("../providers/storage");
15
+ const s3_1 = require("../adapters/storage/s3");
16
+ function findDbConnection(project, connId) {
17
+ return project.dbConnections?.find((c) => c.id === connId);
18
+ }
19
+ function findStorageConnection(project, connId) {
20
+ return project.storageConnections?.find((c) => c.id === connId);
21
+ }
22
+ // `connId` must be the `id` of a real row in project.dbConnections — never a bare
23
+ // config id (config.databases[].id). Builtin rows reuse the shared pooled client keyed by
24
+ // conn.integrationId (config id); BYOC/manual rows open a short-lived client from their own
25
+ // decrypted credential, which the caller must close via `releaseDbClient` once done.
26
+ async function resolveDbConnectionClient(project, connId, config, auth) {
27
+ const conn = findDbConnection(project, connId);
28
+ if (!conn)
29
+ return { error: 'DB connection not found', status: 404 };
30
+ if (conn.source === 'builtin') {
31
+ const builtinId = conn.integrationId;
32
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
33
+ return { error: 'Access to this built-in database is not permitted', status: 403 };
34
+ }
35
+ const db = (config.databases ?? []).find(d => d.id === builtinId);
36
+ if (!db)
37
+ return { error: `Database '${builtinId}' not found in config`, status: 404 };
38
+ try {
39
+ const { client, type } = (0, connectionManager_1.getConnection)(builtinId);
40
+ return { client: client, type, ephemeral: false };
41
+ }
42
+ catch (e) {
43
+ return { error: e.message, status: 503 };
44
+ }
45
+ }
46
+ let connectionString;
47
+ if (conn.source === 'integration') {
48
+ if (!conn.integrationId)
49
+ return { error: 'Connection is missing its integrationId', status: 500 };
50
+ const credential = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
51
+ if (!credential)
52
+ return { error: 'Integration not found', status: 400 };
53
+ connectionString = credential;
54
+ }
55
+ else {
56
+ if (!(0, encryption_1.isEncryptionKeySet)())
57
+ return { error: 'Encryption key is not set', status: 500 };
58
+ if (!conn.encryptedCredential)
59
+ return { error: 'Connection has no stored credential', status: 400 };
60
+ connectionString = (0, encryption_1.decrypt)(conn.encryptedCredential);
61
+ }
62
+ const type = conn.dbType === 'postgresql' ? 'sql' : 'nosql';
63
+ try {
64
+ if (type === 'sql') {
65
+ const pool = new pg_1.Pool({ connectionString });
66
+ await pool.query('SELECT 1');
67
+ return { client: pool, type, ephemeral: true };
68
+ }
69
+ const client = new mongodb_1.MongoClient(connectionString);
70
+ await client.connect();
71
+ return { client, type, ephemeral: true };
72
+ }
73
+ catch (e) {
74
+ return { error: `Failed to connect: ${e.message}`, status: 503 };
75
+ }
76
+ }
77
+ // Only closes clients this resolver opened itself (`ephemeral: true`) — the shared pooled
78
+ // builtin clients are owned by connectionManager and must outlive a single request.
79
+ async function releaseDbClient(resolved) {
80
+ if (!resolved.ephemeral)
81
+ return;
82
+ if (resolved.type === 'sql')
83
+ await resolved.client.end();
84
+ else
85
+ await resolved.client.close();
86
+ }
87
+ async function resolveStorageAdapter(project, connId, config, auth) {
88
+ const conn = findStorageConnection(project, connId);
89
+ if (!conn)
90
+ return { error: 'Storage connection not found', status: 404 };
91
+ if (conn.source === 'builtin') {
92
+ const builtinId = conn.integrationId;
93
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'storage'))) {
94
+ return { error: 'Access to this built-in storage is not permitted', status: 403 };
95
+ }
96
+ const provider = config.storageProviders.find(p => p.id === builtinId);
97
+ if (!provider)
98
+ return { error: 'Storage provider not found', status: 404 };
99
+ if (provider.type === 's3')
100
+ return new s3_1.S3Adapter(provider.config);
101
+ return config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
102
+ }
103
+ if (conn.source === 'integration') {
104
+ if (!conn.integrationId)
105
+ return { error: 'Connection is missing its integrationId', status: 500 };
106
+ try {
107
+ const s3Config = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
108
+ if (!s3Config)
109
+ return { error: 'Integration not found', status: 400 };
110
+ return new s3_1.S3Adapter(s3Config);
111
+ }
112
+ catch (e) {
113
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
114
+ }
115
+ }
116
+ if (!(0, encryption_1.isEncryptionKeySet)())
117
+ return { error: 'Encryption key not set', status: 500 };
118
+ if (!conn.encryptedCredential)
119
+ return { error: 'Connection has no stored credential', status: 400 };
120
+ try {
121
+ const s3Config = JSON.parse((0, encryption_1.decrypt)(conn.encryptedCredential));
122
+ return new s3_1.S3Adapter(s3Config);
123
+ }
124
+ catch (e) {
125
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
126
+ }
127
+ }
@@ -1,2 +1,7 @@
1
1
  import { NodeSchema, StreamByConfig } from '../types';
2
+ export declare class NodeExecutionError extends Error {
3
+ nodeId: string;
4
+ nodeType: string;
5
+ constructor(nodeId: string, nodeType: string, message: string);
6
+ }
2
7
  export declare function executeExport(nodeSchema: NodeSchema, project: any, config: StreamByConfig): Promise<any>;
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeExecutionError = void 0;
3
4
  exports.executeExport = executeExport;
4
- const connectionManager_1 = require("../adapters/database/connectionManager");
5
5
  const encryption_1 = require("../utils/encryption");
6
6
  const dbConnection_1 = require("./dbConnection");
7
+ const connectionResolver_1 = require("./connectionResolver");
7
8
  const jobQueue_1 = require("./jobQueue");
8
9
  const mediaProcessor_1 = require("./mediaProcessor");
9
10
  const vfxProcessor_1 = require("./vfxProcessor");
@@ -75,6 +76,21 @@ function getTarget(nodes, edges, sourceId, sourceHandle) {
75
76
  const edge = edges.find(e => e.source === sourceId && e.sourceHandle === sourceHandle);
76
77
  return edge ? (nodes.find(n => n.id === edge.target) ?? null) : null;
77
78
  }
79
+ class NodeExecutionError extends Error {
80
+ constructor(nodeId, nodeType, message) {
81
+ super(message);
82
+ this.nodeId = nodeId;
83
+ this.nodeType = nodeType;
84
+ this.name = 'NodeExecutionError';
85
+ }
86
+ }
87
+ exports.NodeExecutionError = NodeExecutionError;
88
+ // Export execution is public (gated by allowedOrigin, not user identity — see
89
+ // export.ts:339) — there is no real Auth to pass through. This sentinel satisfies
90
+ // resolveDbConnectionClient's signature without adding a gate that never existed;
91
+ // canUseBuiltin gating is for connection *management* by a user, not for serving an
92
+ // already-published export.
93
+ const SYSTEM_AUTH = { userId: 'system', username: 'system', role: 'admin' };
78
94
  async function executeExport(nodeSchema, project, config) {
79
95
  const nodes = nodeSchema.nodes;
80
96
  const edges = nodeSchema.edges;
@@ -94,42 +110,41 @@ async function executeExport(nodeSchema, project, config) {
94
110
  const tableName = (node.data?.tableName || node.data?.subtitle || node.data?.label);
95
111
  if (!tableName)
96
112
  continue;
113
+ // dataSourceNode always declares its own database connection explicitly (persistence:
114
+ // 'database' in types/nodePersistence.ts) — a project has no notion of a "default"
115
+ // connection, since a project can have many database/storage/API providers connected.
97
116
  const connectionId = node.data?.connectionId;
117
+ if (!connectionId) {
118
+ throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}) has no database connection — connect one on the node.`);
119
+ }
98
120
  const recordId = node.data?.recordId;
99
121
  const projectIdentifier = project._id?.toString() ?? project.id;
100
- const fetchData = async (client, type) => recordId
101
- ? (0, dbConnection_1.queryRecordByIdInternal)(client, type, tableName, recordId, projectIdentifier)
102
- : (0, dbConnection_1.queryRecordsInternal)(client, type, tableName, 500, 0, projectIdentifier);
103
- if (connectionId) {
104
- const { client, type } = (0, connectionManager_1.getConnection)(connectionId);
105
- dataResults.push(await fetchData(client, type));
122
+ const resolved = await (0, connectionResolver_1.resolveDbConnectionClient)(project, connectionId, config, SYSTEM_AUTH);
123
+ if ('error' in resolved) {
124
+ throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): ${resolved.error}`);
125
+ }
126
+ try {
127
+ dataResults.push(recordId
128
+ ? await (0, dbConnection_1.queryRecordByIdInternal)(resolved.client, resolved.type, tableName, recordId, projectIdentifier)
129
+ : await (0, dbConnection_1.queryRecordsInternal)(resolved.client, resolved.type, tableName, 500, 0, projectIdentifier));
106
130
  }
107
- else {
108
- // legacy fallback for nodes saved before connectionId was introduced
109
- const targetDb = config.databases?.find((db) => db.type === project.dbType && db.main)
110
- ?? config.databases?.find((db) => db.type === project.dbType);
111
- if (!targetDb)
112
- throw new Error(`No database connection for type ${project.dbType}`);
113
- const { client, type } = (0, connectionManager_1.getConnection)(targetDb.id);
114
- dataResults.push(await fetchData(client, type));
131
+ finally {
132
+ await (0, connectionResolver_1.releaseDbClient)(resolved);
115
133
  }
116
134
  }
117
- else if (node.type === 'apiConnectionNode') {
118
- const apiConnection = project.apiConnections?.find((c) => c.id === node.data?.connectionId);
119
- if (!apiConnection)
135
+ else if (node.type === 'connectionNode') {
136
+ const connection = project.connections?.find((c) => c.id === node.data?.connectionId);
137
+ if (!connection)
120
138
  continue;
121
139
  const headers = { 'Content-Type': 'application/json' };
122
- if (apiConnection.credentialId && (0, encryption_1.isEncryptionKeySet)()) {
123
- const credential = project.credentials?.find((c) => c.id === apiConnection.credentialId);
124
- if (credential) {
125
- const decrypted = (0, encryption_1.decrypt)(credential.encryptedValue);
126
- const prefix = apiConnection.prefix ? `${apiConnection.prefix} ` : '';
127
- headers['Authorization'] = `${prefix}${decrypted}`;
128
- }
140
+ if (connection.encryptedCredential && (0, encryption_1.isEncryptionKeySet)()) {
141
+ const decrypted = (0, encryption_1.decrypt)(connection.encryptedCredential);
142
+ const prefix = connection.prefix ? `${connection.prefix} ` : '';
143
+ headers['Authorization'] = `${prefix}${decrypted}`;
129
144
  }
130
- const response = await fetch(apiConnection.apiUrl, { method: apiConnection.method || 'GET', headers });
145
+ const response = await fetch(connection.apiUrl, { method: connection.method || 'GET', headers });
131
146
  if (!response.ok)
132
- throw new Error(`API connection fetch failed: ${response.statusText}`);
147
+ throw new Error(`Connection fetch failed: ${response.statusText}`);
133
148
  dataResults.push(await response.json());
134
149
  }
135
150
  }
@@ -173,6 +188,17 @@ async function executeExport(nodeSchema, project, config) {
173
188
  payload = { ...payload, jobId: job.jobId, jobType: 'thumbnail' };
174
189
  }
175
190
  else if (processNode.type === 'ingestNode' && fileId) {
191
+ // ingestNode always declares its own storage connection explicitly (persistence:
192
+ // 'storage' in types/nodePersistence.ts) — a project has no notion of a "default"
193
+ // connection, since a project can have many database/storage/API providers connected.
194
+ const storageConnectionId = nodeData.storageConnectionId;
195
+ if (!storageConnectionId) {
196
+ throw new NodeExecutionError(processNode.id, processNode.type, `Node '${processNode.id}' (${processNode.type}) has no storage connection — connect one on the node.`);
197
+ }
198
+ const storageResolved = await (0, connectionResolver_1.resolveStorageAdapter)(project, storageConnectionId, config, SYSTEM_AUTH);
199
+ if ('error' in storageResolved) {
200
+ throw new NodeExecutionError(processNode.id, processNode.type, `Node '${processNode.id}' (${processNode.type}): ${storageResolved.error}`);
201
+ }
176
202
  const job = (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
177
203
  setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
178
204
  payload = { ...payload, jobId: job.jobId, jobType: 'ingest' };
@@ -22,7 +22,7 @@ async function classifyIntegrationId(id, auth, config, projectId) {
22
22
  kind: 'database',
23
23
  connection: {
24
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',
25
+ projectId, createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
26
26
  },
27
27
  };
28
28
  }
@@ -35,7 +35,7 @@ async function classifyIntegrationId(id, auth, config, projectId) {
35
35
  kind: 'storage',
36
36
  connection: {
37
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',
38
+ projectId, createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
39
39
  },
40
40
  };
41
41
  }
@@ -49,7 +49,7 @@ async function classifyIntegrationId(id, auth, config, projectId) {
49
49
  kind: 'database',
50
50
  connection: {
51
51
  id: crypto_1.default.randomUUID(), name: integration.name, dbType: integration.provider,
52
- credentialId: '', projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
52
+ projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
53
53
  },
54
54
  };
55
55
  }
@@ -57,7 +57,7 @@ async function classifyIntegrationId(id, auth, config, projectId) {
57
57
  kind: 'storage',
58
58
  connection: {
59
59
  id: crypto_1.default.randomUUID(), name: integration.name, type: integration.provider,
60
- credentialId: '', projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
60
+ projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
61
61
  },
62
62
  };
63
63
  }
@@ -12,7 +12,7 @@ export interface StorageConnection {
12
12
  id: string;
13
13
  name: string;
14
14
  type: StorageConnectionType;
15
- credentialId: string;
15
+ encryptedCredential?: string;
16
16
  projectId: string;
17
17
  createdAt: Date;
18
18
  description?: string;
@@ -73,29 +73,35 @@ export interface ProjectInfo {
73
73
  allowUpload?: boolean;
74
74
  allowSharing?: boolean;
75
75
  };
76
- credentials?: Credential[];
77
- apiConnections?: ApiConnection[];
76
+ connections?: Connection[];
78
77
  dbConnections?: DbConnection[];
79
78
  storageConnections?: StorageConnection[];
80
79
  pipelines?: PipelineRef[];
81
80
  builtinBackfilledAt?: Date;
81
+ workflow?: Workflow;
82
82
  }
83
- export interface Credential {
83
+ export type NodePersistence = 'database' | 'storage' | 'both' | 'none';
84
+ export interface Workflow {
84
85
  id: string;
85
- key: string;
86
- encryptedValue: string;
86
+ name: string;
87
+ description?: string;
88
+ status: string;
89
+ projectId: string;
90
+ nodeSchema: NodeSchema | null;
91
+ createdAt: Date;
92
+ updatedAt: Date;
87
93
  }
88
- export type ApiConnectionMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
89
- export interface ApiConnection {
94
+ export type ConnectionMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
95
+ export interface Connection {
90
96
  id: string;
91
97
  name: string;
92
98
  apiUrl: string;
93
- method: ApiConnectionMethod;
99
+ method: ConnectionMethod;
94
100
  projectId: string;
95
101
  createdAt: Date;
96
102
  prefix?: string;
97
103
  description?: string;
98
- credentialId?: string;
104
+ encryptedCredential?: string;
99
105
  }
100
106
  export interface ProjectListInfo {
101
107
  id: string;
@@ -153,7 +159,7 @@ export interface DbConnection {
153
159
  id: string;
154
160
  name: string;
155
161
  dbType: ExternalDbType;
156
- credentialId: string;
162
+ encryptedCredential?: string;
157
163
  projectId: string;
158
164
  createdAt: Date;
159
165
  description?: string;
@@ -214,7 +220,7 @@ export interface Export {
214
220
  fields?: FieldDefinition[];
215
221
  jsonData?: any;
216
222
  apiUrl?: string;
217
- credentialId?: string;
223
+ encryptedCredential?: string;
218
224
  prefix?: string;
219
225
  private?: boolean;
220
226
  allowedOrigin?: string[];
@@ -244,7 +250,7 @@ export interface DistributionConnection {
244
250
  id: string;
245
251
  name: string;
246
252
  target: DistributionTarget;
247
- credentialId?: string;
253
+ encryptedCredential?: string;
248
254
  projectId: string;
249
255
  config: Record<string, string>;
250
256
  createdAt: Date;
@@ -289,7 +295,7 @@ export interface RenderFarmConnection {
289
295
  name: string;
290
296
  provider: RenderFarmProvider;
291
297
  apiUrl: string;
292
- credentialId?: string;
298
+ encryptedCredential?: string;
293
299
  projectId: string;
294
300
  createdAt: Date;
295
301
  description?: string;
@@ -0,0 +1,3 @@
1
+ import { NodePersistence } from './index';
2
+ export declare const NODE_PERSISTENCE: Record<string, NodePersistence>;
3
+ export declare function getNodePersistence(nodeType: string): NodePersistence;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NODE_PERSISTENCE = void 0;
4
+ exports.getNodePersistence = getNodePersistence;
5
+ // Only node types that write to a real database/storage connection need an entry here.
6
+ // Process nodes (transcodeNode, captionNode, etc.) operate on a fileId already resolved by
7
+ // the internal job pipeline (mediaProcessor.ts), not directly on project connections, so
8
+ // they're 'none' — same for filter/deliverable/distribution/annotation/connection nodes.
9
+ exports.NODE_PERSISTENCE = {
10
+ dataSourceNode: 'database',
11
+ ingestNode: 'storage',
12
+ };
13
+ function getNodePersistence(nodeType) {
14
+ return exports.NODE_PERSISTENCE[nodeType] ?? 'none';
15
+ }
@@ -1,2 +1,12 @@
1
+ export declare function sanitizeConnection<T extends {
2
+ encryptedCredential?: string;
3
+ }>(conn: T): Omit<T, 'encryptedCredential'> & {
4
+ hasCredential: boolean;
5
+ };
6
+ export declare function sanitizeConnections<T extends {
7
+ encryptedCredential?: string;
8
+ }>(conns: T[] | undefined): (Omit<T, 'encryptedCredential'> & {
9
+ hasCredential: boolean;
10
+ })[];
1
11
  export declare function sanitizeProject(project: any): any;
2
12
  export declare function sanitizeUserIntegration(integration: any): any;