streamby-core 0.39.0 → 0.40.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +661 -661
  3. package/README.md +680 -573
  4. package/dist/adapters/database/connectionManager.js +179 -124
  5. package/dist/adapters/database/sql.js +1 -1
  6. package/dist/mcp/tools.js +41 -0
  7. package/dist/middleware/createRouter.js +11 -1
  8. package/dist/middleware/routes/ai.js +3 -3
  9. package/dist/middleware/routes/deliverable.d.ts +4 -2
  10. package/dist/middleware/routes/deliverable.js +16 -7
  11. package/dist/middleware/routes/media.js +7 -6
  12. package/dist/middleware/routes/project.js +5 -1
  13. package/dist/middleware/routes/renderFarm.js +4 -77
  14. package/dist/middleware/routes/storage.js +13 -27
  15. package/dist/middleware/routes/worker.d.ts +7 -0
  16. package/dist/middleware/routes/worker.js +125 -0
  17. package/dist/middleware/routes/workerCallback.d.ts +5 -0
  18. package/dist/middleware/routes/workerCallback.js +31 -0
  19. package/dist/models/Model.js +9 -9
  20. package/dist/services/aiProcessor.js +11 -11
  21. package/dist/services/apiTokenPermissions.js +6 -5
  22. package/dist/services/executor.js +10 -10
  23. package/dist/services/jobQueue.d.ts +6 -4
  24. package/dist/services/jobQueue.js +28 -16
  25. package/dist/services/jobStore.d.ts +27 -0
  26. package/dist/services/jobStore.js +177 -0
  27. package/dist/services/mediaProcessor.js +16 -16
  28. package/dist/services/vfxProcessor.js +12 -12
  29. package/dist/services/workerJobs.d.ts +73 -0
  30. package/dist/services/workerJobs.js +459 -0
  31. package/dist/services/workers.d.ts +78 -0
  32. package/dist/services/workers.js +179 -0
  33. package/dist/types/index.d.ts +52 -2
  34. package/dist/utils/assetCategories.d.ts +4 -0
  35. package/dist/utils/assetCategories.js +21 -0
  36. package/dist/utils/builtinAccess.d.ts +1 -1
  37. package/dist/utils/builtinAccess.js +4 -3
  38. package/dist/utils/privateAddress.d.ts +10 -0
  39. package/dist/utils/privateAddress.js +85 -0
  40. package/package.json +1 -1
  41. package/dist/middleware/routes/apiConnection.d.ts +0 -3
  42. package/dist/middleware/routes/apiConnection.js +0 -100
  43. package/dist/middleware/routes/credential.d.ts +0 -3
  44. package/dist/middleware/routes/credential.js +0 -80
  45. package/dist/services/apiConnection.d.ts +0 -18
  46. package/dist/services/apiConnection.js +0 -65
  47. package/dist/services/credential.d.ts +0 -8
  48. package/dist/services/credential.js +0 -70
  49. package/dist/services/pipeline.d.ts +0 -2
  50. package/dist/services/pipeline.js +0 -292
  51. package/dist/utils/crypto.d.ts +0 -3
  52. package/dist/utils/crypto.js +0 -32
@@ -41,159 +41,214 @@ const getConnection = (id) => {
41
41
  return clientEntry;
42
42
  };
43
43
  exports.getConnection = getConnection;
44
+ // StreamBy's tables, made (or brought up to date) at startup. Several instances starting at once
45
+ // against the same database would deadlock on these statements, so one runs them at a time:
46
+ // in a transaction holding a transaction-level advisory lock, which ends with it. (A session
47
+ // lock could outlive its unlock behind a pooler in transaction mode, such as Supabase's, which
48
+ // may run the unlock on another connection, and block every later startup.)
44
49
  const ensureTablesExist = async (pool) => {
50
+ const client = await pool.connect();
45
51
  try {
46
- await pool.query(`
47
- CREATE SCHEMA IF NOT EXISTS streamby;
48
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
52
+ await client.query('BEGIN');
53
+ await client.query(`SELECT pg_advisory_xact_lock(hashtext('streamby:ensure-tables'))`);
54
+ await createTables(client);
55
+ await client.query('COMMIT');
56
+ }
57
+ catch (error) {
58
+ await client.query('ROLLBACK').catch(() => { });
59
+ throw error;
60
+ }
61
+ finally {
62
+ client.release();
63
+ }
64
+ };
65
+ const createTables = async (pool) => {
66
+ try {
67
+ await pool.query(`
68
+ CREATE SCHEMA IF NOT EXISTS streamby;
69
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
49
70
  `);
50
71
  // Migrate id column from UUID to TEXT if it was created with the old schema
51
- await pool.query(`
52
- DO $$ BEGIN
53
- ALTER TABLE streamby.projects ALTER COLUMN id TYPE TEXT USING id::text;
54
- ALTER TABLE streamby.projects ALTER COLUMN id SET DEFAULT uuid_generate_v4()::text;
55
- EXCEPTION WHEN OTHERS THEN NULL; END $$;
72
+ await pool.query(`
73
+ DO $$ BEGIN
74
+ ALTER TABLE streamby.projects ALTER COLUMN id TYPE TEXT USING id::text;
75
+ ALTER TABLE streamby.projects ALTER COLUMN id SET DEFAULT uuid_generate_v4()::text;
76
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
56
77
  `);
57
- await pool.query(`
58
- CREATE TABLE IF NOT EXISTS streamby.projects (
59
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
60
- name VARCHAR(255) NOT NULL,
61
- description TEXT,
62
- image TEXT,
63
- "allowedOrigin" JSONB DEFAULT '[]',
64
- exports JSONB DEFAULT '[]',
65
- connections JSONB DEFAULT '[]',
66
- "dbConnections" JSONB DEFAULT '[]',
67
- "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
68
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
69
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
70
- );
78
+ await pool.query(`
79
+ CREATE TABLE IF NOT EXISTS streamby.projects (
80
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
81
+ name VARCHAR(255) NOT NULL,
82
+ description TEXT,
83
+ image TEXT,
84
+ "allowedOrigin" JSONB DEFAULT '[]',
85
+ exports JSONB DEFAULT '[]',
86
+ connections JSONB DEFAULT '[]',
87
+ "dbConnections" JSONB DEFAULT '[]',
88
+ "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
89
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
90
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
91
+ );
71
92
  `);
72
93
  console.log('✅ "projects" table ensured to exist.');
73
94
  // Add JSONB columns to existing tables that predate this schema version
74
- await pool.query(`
75
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
76
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
77
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS connections JSONB DEFAULT '[]';
78
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
79
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
80
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
81
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
82
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
83
- -- Written when a project is made (routes/project) and by its connection routes.
84
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS public BOOLEAN DEFAULT TRUE;
85
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS category TEXT;
86
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "integrationConnections" JSONB DEFAULT '[]';
87
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "distributionConnections" JSONB DEFAULT '[]';
88
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "renderFarmConnections" JSONB DEFAULT '[]';
95
+ await pool.query(`
96
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
97
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
98
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS connections JSONB DEFAULT '[]';
99
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
100
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
101
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
102
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
103
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
104
+ -- Written when a project is made (routes/project) and by its connection routes.
105
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS public BOOLEAN DEFAULT TRUE;
106
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS category TEXT;
107
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "integrationConnections" JSONB DEFAULT '[]';
108
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "distributionConnections" JSONB DEFAULT '[]';
109
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "renderFarmConnections" JSONB DEFAULT '[]';
89
110
  `);
90
111
  // Ensure dbType exists with a non-null default
91
- await pool.query(`
92
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
112
+ await pool.query(`
113
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
93
114
  `);
94
- await pool.query(`
95
- CREATE TABLE IF NOT EXISTS streamby.project_members (
96
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
97
- "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
98
- "userId" VARCHAR(255) NOT NULL,
99
- role VARCHAR(255) NOT NULL DEFAULT 'member',
100
- status VARCHAR(50) NOT NULL DEFAULT 'active',
101
- archived BOOLEAN NOT NULL DEFAULT FALSE,
102
- "archivedBy" VARCHAR(255),
103
- "archivedAt" TIMESTAMP WITH TIME ZONE,
104
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
105
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
106
- UNIQUE ("projectId", "userId")
107
- );
115
+ await pool.query(`
116
+ CREATE TABLE IF NOT EXISTS streamby.project_members (
117
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
118
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
119
+ "userId" VARCHAR(255) NOT NULL,
120
+ role VARCHAR(255) NOT NULL DEFAULT 'member',
121
+ status VARCHAR(50) NOT NULL DEFAULT 'active',
122
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
123
+ "archivedBy" VARCHAR(255),
124
+ "archivedAt" TIMESTAMP WITH TIME ZONE,
125
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
126
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
127
+ UNIQUE ("projectId", "userId")
128
+ );
108
129
  `);
109
- await pool.query(`
110
- ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
130
+ await pool.query(`
131
+ ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
111
132
  `);
112
133
  console.log('✅ "project_members" table ensured to exist.');
113
134
  // Migrate projectId FK column from UUID to TEXT if table predates this schema version
114
- await pool.query(`
115
- DO $$ BEGIN
116
- ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
117
- ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
118
- ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
119
- FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
120
- EXCEPTION WHEN OTHERS THEN NULL; END $$;
135
+ await pool.query(`
136
+ DO $$ BEGIN
137
+ ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
138
+ ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
139
+ ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
140
+ FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
141
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
121
142
  `);
122
- await pool.query(`
123
- CREATE TABLE IF NOT EXISTS streamby.exports (
124
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
125
- "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
126
- status VARCHAR(50) NOT NULL DEFAULT 'pending',
127
- "filePath" TEXT,
128
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
129
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
130
- );
143
+ await pool.query(`
144
+ CREATE TABLE IF NOT EXISTS streamby.exports (
145
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
146
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
147
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
148
+ "filePath" TEXT,
149
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
150
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
151
+ );
131
152
  `);
132
153
  console.log('✅ "exports" table ensured to exist.');
133
154
  // user_integrations is registered against the main db only (see createRouter.ts), but
134
155
  // this function runs per-SQL-connection like projects/exports above — harmless on
135
156
  // non-main connections since the model never queries them there.
136
- await pool.query(`
137
- CREATE TABLE IF NOT EXISTS streamby.user_integrations (
138
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
139
- "userId" VARCHAR(255) NOT NULL,
140
- kind VARCHAR(20) NOT NULL,
141
- provider VARCHAR(20) NOT NULL,
142
- name VARCHAR(255) NOT NULL,
143
- description TEXT,
144
- "encryptedCredential" TEXT NOT NULL,
145
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
146
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
147
- );
148
- CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
157
+ await pool.query(`
158
+ CREATE TABLE IF NOT EXISTS streamby.user_integrations (
159
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
160
+ "userId" VARCHAR(255) NOT NULL,
161
+ kind VARCHAR(20) NOT NULL,
162
+ provider VARCHAR(20) NOT NULL,
163
+ name VARCHAR(255) NOT NULL,
164
+ description TEXT,
165
+ "encryptedCredential" TEXT NOT NULL,
166
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
167
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
168
+ );
169
+ CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
149
170
  `);
150
171
  console.log('✅ "user_integrations" table ensured to exist.');
151
172
  // API tokens: only their hash is kept (see services/apiTokens). Main db only, like
152
173
  // user_integrations above. A table from before tokens had a kind and permissions gets the
153
174
  // new columns; its old tokens no longer work (they have no kind) and aren't listed.
154
- await pool.query(`
155
- CREATE TABLE IF NOT EXISTS streamby.api_tokens (
156
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
157
- kind VARCHAR(10),
158
- "userId" VARCHAR(255) NOT NULL,
159
- "createdByName" VARCHAR(255),
160
- name VARCHAR(255) NOT NULL,
161
- prefix VARCHAR(32) NOT NULL,
162
- hash CHAR(64) NOT NULL UNIQUE,
163
- permissions JSONB NOT NULL DEFAULT '[]',
164
- "projectIds" JSONB NOT NULL,
165
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
166
- "expiresAt" TIMESTAMP WITH TIME ZONE,
167
- "lastUsedAt" TIMESTAMP WITH TIME ZONE
168
- );
169
- ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS kind VARCHAR(10);
170
- ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS "createdByName" VARCHAR(255);
171
- ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]';
172
- DO $$ BEGIN
173
- IF EXISTS (SELECT 1 FROM information_schema.columns
174
- WHERE table_schema = 'streamby' AND table_name = 'api_tokens' AND column_name = 'access') THEN
175
- ALTER TABLE streamby.api_tokens ALTER COLUMN access DROP NOT NULL;
176
- END IF;
177
- END $$;
178
- CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
179
- CREATE INDEX IF NOT EXISTS api_tokens_kind_idx ON streamby.api_tokens (kind);
175
+ await pool.query(`
176
+ CREATE TABLE IF NOT EXISTS streamby.api_tokens (
177
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
178
+ kind VARCHAR(10),
179
+ "userId" VARCHAR(255) NOT NULL,
180
+ "createdByName" VARCHAR(255),
181
+ name VARCHAR(255) NOT NULL,
182
+ prefix VARCHAR(32) NOT NULL,
183
+ hash CHAR(64) NOT NULL UNIQUE,
184
+ permissions JSONB NOT NULL DEFAULT '[]',
185
+ "projectIds" JSONB NOT NULL,
186
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
187
+ "expiresAt" TIMESTAMP WITH TIME ZONE,
188
+ "lastUsedAt" TIMESTAMP WITH TIME ZONE
189
+ );
190
+ ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS kind VARCHAR(10);
191
+ ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS "createdByName" VARCHAR(255);
192
+ ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]';
193
+ DO $$ BEGIN
194
+ IF EXISTS (SELECT 1 FROM information_schema.columns
195
+ WHERE table_schema = 'streamby' AND table_name = 'api_tokens' AND column_name = 'access') THEN
196
+ ALTER TABLE streamby.api_tokens ALTER COLUMN access DROP NOT NULL;
197
+ END IF;
198
+ END $$;
199
+ CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
200
+ CREATE INDEX IF NOT EXISTS api_tokens_kind_idx ON streamby.api_tokens (kind);
180
201
  `);
181
202
  console.log('✅ "api_tokens" table ensured to exist.');
203
+ // Jobs (see services/jobStore): kept here so they survive restarts and every instance sees
204
+ // them. Main db only, like the tables above.
205
+ await pool.query(`
206
+ CREATE TABLE IF NOT EXISTS streamby.jobs (
207
+ id TEXT PRIMARY KEY,
208
+ "jobType" VARCHAR(40) NOT NULL,
209
+ "userId" VARCHAR(255) NOT NULL,
210
+ "projectId" VARCHAR(255) NOT NULL,
211
+ "assetId" VARCHAR(255),
212
+ status VARCHAR(20) NOT NULL,
213
+ stage VARCHAR(60) NOT NULL,
214
+ progress INTEGER NOT NULL DEFAULT 0,
215
+ payload JSONB NOT NULL DEFAULT '{}',
216
+ error TEXT,
217
+ worker JSONB,
218
+ kind VARCHAR(100),
219
+ profile JSONB,
220
+ "versionId" VARCHAR(255),
221
+ "resultMode" VARCHAR(20),
222
+ attempts INTEGER NOT NULL DEFAULT 0,
223
+ "maxAttempts" INTEGER NOT NULL DEFAULT 1,
224
+ "nextAttemptAt" TIMESTAMP WITH TIME ZONE,
225
+ "leaseUntil" TIMESTAMP WITH TIME ZONE,
226
+ "callbackHash" CHAR(64),
227
+ upload JSONB,
228
+ report JSONB,
229
+ result JSONB,
230
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
231
+ "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
232
+ );
233
+ CREATE INDEX IF NOT EXISTS jobs_project_idx ON streamby.jobs ("projectId", "createdAt");
234
+ CREATE INDEX IF NOT EXISTS jobs_due_idx ON streamby.jobs (status, "nextAttemptAt") WHERE worker IS NOT NULL;
235
+ `);
236
+ console.log('✅ "jobs" table ensured to exist.');
182
237
  // The MCP server's audit log: every tool call (see mcp/audit). Main db only.
183
- await pool.query(`
184
- CREATE TABLE IF NOT EXISTS streamby.mcp_audit (
185
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
186
- "userId" VARCHAR(255) NOT NULL,
187
- "tokenId" VARCHAR(255) NOT NULL,
188
- "projectId" VARCHAR(255),
189
- tool VARCHAR(100) NOT NULL,
190
- ok BOOLEAN NOT NULL,
191
- status INTEGER,
192
- error TEXT,
193
- "durationMs" INTEGER NOT NULL,
194
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
195
- );
196
- CREATE INDEX IF NOT EXISTS mcp_audit_token_idx ON streamby.mcp_audit ("tokenId", "createdAt");
238
+ await pool.query(`
239
+ CREATE TABLE IF NOT EXISTS streamby.mcp_audit (
240
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
241
+ "userId" VARCHAR(255) NOT NULL,
242
+ "tokenId" VARCHAR(255) NOT NULL,
243
+ "projectId" VARCHAR(255),
244
+ tool VARCHAR(100) NOT NULL,
245
+ ok BOOLEAN NOT NULL,
246
+ status INTEGER,
247
+ error TEXT,
248
+ "durationMs" INTEGER NOT NULL,
249
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
250
+ );
251
+ CREATE INDEX IF NOT EXISTS mcp_audit_token_idx ON streamby.mcp_audit ("tokenId", "createdAt");
197
252
  `);
198
253
  console.log('✅ "mcp_audit" table ensured to exist.');
199
254
  }
@@ -10,7 +10,7 @@ function serializeForPg(v) {
10
10
  }
11
11
  const createSQLExportTable = async (connection, projectId, exportName, nodeSchema) => {
12
12
  const exportId = (0, crypto_1.randomUUID)();
13
- await connection.query(`INSERT INTO "exports" (id, "projectId", node_schema, name, method, created_at, updated_at)
13
+ await connection.query(`INSERT INTO "exports" (id, "projectId", node_schema, name, method, created_at, updated_at)
14
14
  VALUES ($1, $2, $3, $4, 'GET', NOW(), NOW())`, [exportId, projectId, nodeSchema ? JSON.stringify(nodeSchema) : null, exportName]);
15
15
  return { exportId };
16
16
  };
package/dist/mcp/tools.js CHANGED
@@ -317,6 +317,47 @@ exports.TOOLS = [
317
317
  projectOf: byProject,
318
318
  call: ({ projectId, deliverableId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/deliverables/${enc(deliverableId)}` }),
319
319
  }),
320
+ // ── Processing (workers) ──
321
+ tool({
322
+ name: 'list_workers', title: 'List workers', permissions: ['project:read'],
323
+ description: "The processing workers a project can use: the host's and the project's own (each one's id, source, what it does — its kinds — and the asset categories it takes). They show in the project's Workers settings.",
324
+ input: { projectId },
325
+ projectOf: byProject,
326
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/workers` }),
327
+ pick: (data) => data?.data ?? [],
328
+ }),
329
+ tool({
330
+ name: 'process_asset', title: 'Process an asset', permissions: ['jobs:run'], write: true,
331
+ description: "Asks a worker to process an asset (or one of its versions), e.g. optimize a glTF. The result is kept as a new asset derived from it (resultMode 'derived', the default: the original is untouched) or as the asset's new version ('version': its URL then serves the result). It runs in the background: follow it with get_job or list_jobs; it shows in the file's Jobs tab, in Storage.",
332
+ input: {
333
+ projectId,
334
+ assetId: id('asset'),
335
+ versionId: zod_1.z.string().optional().describe('A version (list_asset_versions); the latest if not given'),
336
+ worker: zod_1.z.object({ source: zod_1.z.enum(['builtin', 'project']), id: zod_1.z.string().min(1) }).describe('A worker, from list_workers'),
337
+ kind: zod_1.z.string().min(1).describe("What it should do: one of the worker's kinds"),
338
+ profile: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Options for the worker (e.g. { \"textureFormat\": \"ktx2\" })"),
339
+ resultMode: zod_1.z.enum(['derived', 'version']).optional(),
340
+ },
341
+ projectOf: byProject,
342
+ call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/jobs/process`, body: defined(fields) }),
343
+ pick: (data) => data?.job,
344
+ }),
345
+ tool({
346
+ name: 'list_jobs', title: 'List jobs', permissions: ['jobs:read'],
347
+ description: "A project's jobs, newest first (of one asset if given): their status, stage, progress, error and, when done, their worker's report and result.",
348
+ input: { projectId, assetId: zod_1.z.string().optional() },
349
+ projectOf: byProject,
350
+ call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/jobs${assetId ? `?${new URLSearchParams({ assetId })}` : ''}` }),
351
+ pick: (data) => data?.data ?? [],
352
+ }),
353
+ tool({
354
+ name: 'retry_job', title: 'Retry a job', permissions: ['jobs:run'], write: true,
355
+ description: "Sends a failed worker job to its worker again.",
356
+ input: { projectId, jobId: id('job') },
357
+ projectOf: byProject,
358
+ call: ({ projectId, jobId }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/jobs/${enc(jobId)}/retry` }),
359
+ pick: (data) => data?.job,
360
+ }),
320
361
  // ── Projects ──
321
362
  tool({
322
363
  name: 'create_project', title: 'Create a project', permissions: ['projects:create'], write: true,
@@ -15,6 +15,7 @@ const auth_1 = require("../services/auth");
15
15
  const apiToken_1 = require("./routes/apiToken");
16
16
  const mcp_1 = require("./routes/mcp");
17
17
  const encryption_1 = require("../utils/encryption");
18
+ const jobStore_1 = require("../services/jobStore");
18
19
  const auth_2 = require("./routes/auth");
19
20
  const user_1 = require("./routes/user");
20
21
  const export_1 = require("./routes/export");
@@ -38,11 +39,15 @@ const production_1 = require("./routes/production");
38
39
  const rights_1 = require("./routes/rights");
39
40
  const workflow_1 = require("./routes/workflow");
40
41
  const pipeline_1 = require("./routes/pipeline");
42
+ const worker_1 = require("./routes/worker");
43
+ const workerCallback_1 = require("./routes/workerCallback");
44
+ const workerJobs_1 = require("../services/workerJobs");
41
45
  const deliverable_1 = require("./routes/deliverable");
42
46
  function createStreamByRouter(config) {
43
47
  const router = express_1.default.Router();
44
48
  if (config.encrypt)
45
49
  (0, encryption_1.setEncryptionKey)(config.encrypt, config.encryptPreviousKeys);
50
+ (0, jobStore_1.setJobStoreDatabase)(config);
46
51
  // Establish DB connections asynchronously (Postgres/Mongo + schema migrations).
47
52
  // Until they're ready, HOLD incoming requests instead of letting them through —
48
53
  // route handlers resolve projects/exports (e.g. the /menu-list export) against a
@@ -69,6 +74,8 @@ function createStreamByRouter(config) {
69
74
  })
70
75
  .finally(() => {
71
76
  ready = true;
77
+ // Jobs left pending (or with their worker's time up) by a restart are sent again.
78
+ (0, workerJobs_1.startWorkerSweeper)({ ...config, adapter });
72
79
  });
73
80
  router.use((_req, _res, next) => {
74
81
  if (ready)
@@ -108,6 +115,8 @@ function createStreamByRouter(config) {
108
115
  // /auth answers whether there's a session (it reads it itself); everything after needs one,
109
116
  // but for the public export route (see authenticate).
110
117
  router.use((0, auth_2.authRouter)(config));
118
+ // Workers call back with their job's token, not a session (see routes/workerCallback).
119
+ router.use((0, workerCallback_1.workerCallbackRouter)({ ...config, adapter }));
111
120
  router.use((0, auth_1.authenticate)(config));
112
121
  router.use(auth_1.enforceApiTokenScope);
113
122
  router.use(subscription_1.ensureSubscription);
@@ -135,6 +144,7 @@ function createStreamByRouter(config) {
135
144
  router.use((0, rights_1.rightsRouter)(config));
136
145
  router.use((0, workflow_1.workflowRouter)(config));
137
146
  router.use((0, pipeline_1.pipelineRouter)(config));
138
- router.use((0, deliverable_1.deliverableRouter)(config));
147
+ router.use((0, worker_1.workerRouter)({ ...config, adapter }));
148
+ router.use((0, deliverable_1.deliverableRouter)({ ...config, adapter }));
139
149
  return router;
140
150
  }
@@ -31,7 +31,7 @@ function aiRouter(_config) {
31
31
  return res.status(404).json({ message: 'Project not found' });
32
32
  if (!(0, auth_1.canEditProject)(project, auth.userId))
33
33
  return res.status(403).json({ message: 'Unauthorized' });
34
- const job = (0, jobQueue_1.createJob)('transcription', auth.userId, projectId, { fileId, model, sourceLanguage, outputFormats, provider, credentialId });
34
+ const job = await (0, jobQueue_1.createJob)('transcription', auth.userId, projectId, { fileId, model, sourceLanguage, outputFormats, provider, credentialId });
35
35
  setImmediate(() => (0, aiProcessor_1.runTranscriptionJob)(job.jobId, fileId, projectId, { model, sourceLanguage, outputFormats, provider, credentialId }));
36
36
  res.status(202).json({ jobId: job.jobId, status: job.status });
37
37
  }
@@ -52,7 +52,7 @@ function aiRouter(_config) {
52
52
  return res.status(404).json({ message: 'Project not found' });
53
53
  if (!(0, auth_1.canEditProject)(project, auth.userId))
54
54
  return res.status(403).json({ message: 'Unauthorized' });
55
- const job = (0, jobQueue_1.createJob)('upscale', auth.userId, projectId, { fileId, scale, model, mode, credentialId });
55
+ const job = await (0, jobQueue_1.createJob)('upscale', auth.userId, projectId, { fileId, scale, model, mode, credentialId });
56
56
  setImmediate(() => (0, aiProcessor_1.runUpscaleJob)(job.jobId, fileId, projectId, { scale, model, mode, credentialId }));
57
57
  res.status(202).json({ jobId: job.jobId, status: job.status });
58
58
  }
@@ -73,7 +73,7 @@ function aiRouter(_config) {
73
73
  return res.status(404).json({ message: 'Project not found' });
74
74
  if (!(0, auth_1.canEditProject)(project, auth.userId))
75
75
  return res.status(403).json({ message: 'Unauthorized' });
76
- const job = (0, jobQueue_1.createJob)('generate-asset', auth.userId, projectId, { assetType, provider, prompt, seed, credentialId });
76
+ const job = await (0, jobQueue_1.createJob)('generate-asset', auth.userId, projectId, { assetType, provider, prompt, seed, credentialId });
77
77
  setImmediate(() => (0, aiProcessor_1.runGenerateAssetJob)(job.jobId, projectId, { assetType, provider, prompt, seed, credentialId }));
78
78
  res.status(202).json({ jobId: job.jobId, status: job.status });
79
79
  }
@@ -1,3 +1,5 @@
1
1
  import { Router } from 'express';
2
- import { StreamByConfig } from '../../types';
3
- export declare function deliverableRouter(_config: StreamByConfig): Router;
2
+ import { StorageAdapter, StreamByConfig } from '../../types';
3
+ export declare function deliverableRouter(config: StreamByConfig & {
4
+ adapter?: StorageAdapter;
5
+ }): Router;
@@ -2,15 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deliverableRouter = deliverableRouter;
4
4
  const express_1 = require("express");
5
+ const workerJobs_1 = require("../../services/workerJobs");
6
+ const worker_1 = require("./worker");
7
+ const auth_1 = require("../../services/auth");
5
8
  const manager_1 = require("../../models/manager");
6
- const auth_1 = require("../../utils/auth");
9
+ const auth_2 = require("../../utils/auth");
7
10
  const nosqlDb_1 = require("../../utils/nosqlDb");
8
11
  const assetVersions_1 = require("../../services/assetVersions");
9
12
  const deliverables_1 = require("../../services/deliverables");
10
13
  const MAX_NOTE = 1000;
11
14
  // A project's deliverables (see services/deliverables): marked, listed and removed. Removing
12
- // one never deletes its asset.
13
- function deliverableRouter(_config) {
15
+ // one never deletes its asset. Marking one starts the workers set to process its kind of asset
16
+ // by themselves.
17
+ function deliverableRouter(config) {
14
18
  const router = (0, express_1.Router)();
15
19
  const Project = (0, manager_1.getModel)('projects');
16
20
  const exportsOf = (project) => project.exports ?? [];
@@ -48,7 +52,7 @@ function deliverableRouter(_config) {
48
52
  const project = await Project.findOne({ _id: projectId });
49
53
  if (!project)
50
54
  return res.status(404).json({ message: 'Project not found' });
51
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
55
+ if (!(0, auth_2.isProjectMember)(project, auth.userId))
52
56
  return res.status(403).json({ message: 'Unauthorized' });
53
57
  const deliverables = await (0, deliverables_1.listDeliverables)(projectId, { shotId, exportId });
54
58
  res.json({ deliverables: await describe(project, deliverables) });
@@ -77,7 +81,7 @@ function deliverableRouter(_config) {
77
81
  const project = await Project.findOne({ _id: projectId });
78
82
  if (!project)
79
83
  return res.status(404).json({ message: 'Project not found' });
80
- if (!(0, auth_1.canEditProject)(project, auth.userId))
84
+ if (!(0, auth_2.canEditProject)(project, auth.userId))
81
85
  return res.status(403).json({ message: 'Unauthorized' });
82
86
  const db = (0, nosqlDb_1.nosqlDb)();
83
87
  if (!db)
@@ -104,7 +108,12 @@ function deliverableRouter(_config) {
104
108
  createdBy: auth.userId,
105
109
  });
106
110
  const [described] = await describe(project, [deliverable]);
107
- res.status(201).json({ deliverable: described });
111
+ // The project's workers set to process its kind of asset by themselves start on it: not
112
+ // for an API token that can't run jobs.
113
+ const jobs = (0, auth_1.tokenAllows)(auth.apiToken, 'jobs:run')
114
+ ? await (0, workerJobs_1.enqueueAutomaticJobs)(project, assetId, versionId || null, auth, config, (0, worker_1.callbackBaseOf)(req, config)).catch(() => [])
115
+ : [];
116
+ res.status(201).json({ deliverable: described, jobs: jobs.map(workerJobs_1.publicJob) });
108
117
  }
109
118
  catch (err) {
110
119
  res.status(500).json({ message: 'Failed to mark the deliverable', details: err.message });
@@ -117,7 +126,7 @@ function deliverableRouter(_config) {
117
126
  const project = await Project.findOne({ _id: projectId });
118
127
  if (!project)
119
128
  return res.status(404).json({ message: 'Project not found' });
120
- if (!(0, auth_1.canEditProject)(project, auth.userId))
129
+ if (!(0, auth_2.canEditProject)(project, auth.userId))
121
130
  return res.status(403).json({ message: 'Unauthorized' });
122
131
  if (!(await (0, deliverables_1.deleteDeliverable)(projectId, deliverableId)))
123
132
  return res.status(404).json({ message: 'Deliverable not found' });
@@ -8,6 +8,7 @@ const jobQueue_1 = require("../../services/jobQueue");
8
8
  const mediaProcessor_1 = require("../../services/mediaProcessor");
9
9
  const storage_1 = require("../../providers/storage");
10
10
  const connectionManager_1 = require("../../adapters/database/connectionManager");
11
+ const workerJobs_1 = require("../../services/workerJobs");
11
12
  const auth_2 = require("../../services/auth");
12
13
  function getMetadataCollection() {
13
14
  const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
@@ -31,7 +32,7 @@ function mediaRouter(config) {
31
32
  router.get('/jobs/:jobId', async (req, res) => {
32
33
  try {
33
34
  const auth = req.auth;
34
- const job = (0, jobQueue_1.getJob)(req.params.jobId);
35
+ const job = await (0, jobQueue_1.getJob)(req.params.jobId);
35
36
  if (!job)
36
37
  return res.status(404).json({ message: 'Job not found' });
37
38
  // With an API token, only a job of a project it reaches.
@@ -42,7 +43,7 @@ function mediaRouter(config) {
42
43
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
43
44
  return res.status(404).json({ message: 'Job not found' });
44
45
  }
45
- res.json({ job });
46
+ res.json({ job: (0, workerJobs_1.publicJob)(job) });
46
47
  }
47
48
  catch (err) {
48
49
  res.status(500).json({ message: 'Failed to fetch job', details: err.message });
@@ -61,7 +62,7 @@ function mediaRouter(config) {
61
62
  return res.status(404).json({ message: 'Project not found' });
62
63
  if (!(0, auth_1.canEditProject)(project, auth.userId))
63
64
  return res.status(403).json({ message: 'Unauthorized' });
64
- const job = (0, jobQueue_1.createJob)('ingest', auth.userId, projectId, { fileId });
65
+ const job = await (0, jobQueue_1.createJob)('ingest', auth.userId, projectId, { fileId });
65
66
  // Run async — response returns immediately with jobId
66
67
  setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
67
68
  res.status(202).json({ jobId: job.jobId, status: job.status });
@@ -83,7 +84,7 @@ function mediaRouter(config) {
83
84
  return res.status(404).json({ message: 'Project not found' });
84
85
  if (!(0, auth_1.canEditProject)(project, auth.userId))
85
86
  return res.status(403).json({ message: 'Unauthorized' });
86
- const job = (0, jobQueue_1.createJob)('transcode', auth.userId, projectId, {
87
+ const job = await (0, jobQueue_1.createJob)('transcode', auth.userId, projectId, {
87
88
  fileId, codec, resolution, outputFormat, bitrate, audioCodec,
88
89
  });
89
90
  setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, { codec, resolution, outputFormat, bitrate, audioCodec }, adapter));
@@ -106,7 +107,7 @@ function mediaRouter(config) {
106
107
  return res.status(404).json({ message: 'Project not found' });
107
108
  if (!(0, auth_1.canEditProject)(project, auth.userId))
108
109
  return res.status(403).json({ message: 'Unauthorized' });
109
- const job = (0, jobQueue_1.createJob)('thumbnail', auth.userId, projectId, { fileId, timecode, resolution, strategy });
110
+ const job = await (0, jobQueue_1.createJob)('thumbnail', auth.userId, projectId, { fileId, timecode, resolution, strategy });
110
111
  setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, { timecode, resolution, strategy }));
111
112
  res.status(202).json({ jobId: job.jobId, status: job.status });
112
113
  }
@@ -127,7 +128,7 @@ function mediaRouter(config) {
127
128
  return res.status(404).json({ message: 'Project not found' });
128
129
  if (!(0, auth_1.canEditProject)(project, auth.userId))
129
130
  return res.status(403).json({ message: 'Unauthorized' });
130
- const job = (0, jobQueue_1.createJob)('caption', auth.userId, projectId, { fileId, sourceLanguage, outputFormat, provider });
131
+ const job = await (0, jobQueue_1.createJob)('caption', auth.userId, projectId, { fileId, sourceLanguage, outputFormat, provider });
131
132
  setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, { sourceLanguage, outputFormat, provider }));
132
133
  res.status(202).json({ jobId: job.jobId, status: job.status });
133
134
  }
@@ -12,6 +12,7 @@ const auth_2 = require("../../services/auth");
12
12
  const apiTokens_1 = require("../../services/apiTokens");
13
13
  const nosqlDb_1 = require("../../utils/nosqlDb");
14
14
  const production_1 = require("../../services/production");
15
+ const jobStore_1 = require("../../services/jobStore");
15
16
  function projectRouter(config) {
16
17
  const router = (0, express_1.Router)();
17
18
  const Project = (0, manager_1.getModel)('projects');
@@ -254,9 +255,12 @@ function projectRouter(config) {
254
255
  return res.status(403).json({ message: 'Only a project admin can delete it' });
255
256
  }
256
257
  await Project.useDbType(project.dbType).delete({ _id: projectId });
257
- // Its tokens have nothing left to reach, and its production data nothing to belong to.
258
+ // Its tokens have nothing left to reach, and its production data and jobs nothing to
259
+ // belong to.
258
260
  await (0, apiTokens_1.deleteProjectApiTokens)(String(projectId));
259
261
  await (0, production_1.deleteProductionData)(String(projectId));
262
+ if (projectId)
263
+ await (0, jobStore_1.jobStore)().deleteProject(String(projectId));
260
264
  const allProjects = await Project.find({});
261
265
  const projects = allProjects
262
266
  .filter(project => {