streamby-core 0.38.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 (40) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +108 -1
  3. package/dist/adapters/database/connectionManager.js +55 -0
  4. package/dist/mcp/tools.js +41 -0
  5. package/dist/middleware/createRouter.js +11 -1
  6. package/dist/middleware/routes/ai.js +3 -3
  7. package/dist/middleware/routes/deliverable.d.ts +4 -2
  8. package/dist/middleware/routes/deliverable.js +16 -7
  9. package/dist/middleware/routes/export.js +16 -0
  10. package/dist/middleware/routes/media.js +7 -6
  11. package/dist/middleware/routes/project.js +36 -10
  12. package/dist/middleware/routes/renderFarm.js +4 -77
  13. package/dist/middleware/routes/storage.js +13 -27
  14. package/dist/middleware/routes/worker.d.ts +7 -0
  15. package/dist/middleware/routes/worker.js +125 -0
  16. package/dist/middleware/routes/workerCallback.d.ts +5 -0
  17. package/dist/middleware/routes/workerCallback.js +31 -0
  18. package/dist/services/aiProcessor.js +11 -11
  19. package/dist/services/apiTokenPermissions.js +8 -7
  20. package/dist/services/executor.js +10 -10
  21. package/dist/services/jobQueue.d.ts +6 -4
  22. package/dist/services/jobQueue.js +28 -16
  23. package/dist/services/jobStore.d.ts +27 -0
  24. package/dist/services/jobStore.js +177 -0
  25. package/dist/services/mediaProcessor.js +16 -16
  26. package/dist/services/vfxProcessor.js +12 -12
  27. package/dist/services/workerJobs.d.ts +73 -0
  28. package/dist/services/workerJobs.js +459 -0
  29. package/dist/services/workers.d.ts +78 -0
  30. package/dist/services/workers.js +179 -0
  31. package/dist/types/index.d.ts +52 -2
  32. package/dist/utils/assetCategories.d.ts +4 -0
  33. package/dist/utils/assetCategories.js +21 -0
  34. package/dist/utils/builtinAccess.d.ts +1 -1
  35. package/dist/utils/builtinAccess.js +4 -3
  36. package/dist/utils/privateAddress.d.ts +10 -0
  37. package/dist/utils/privateAddress.js +85 -0
  38. package/dist/utils/sanitize.d.ts +1 -0
  39. package/dist/utils/sanitize.js +14 -0
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.40.0
4
+
5
+ ### ⚠️ Breaking changes
6
+
7
+ - **A project's render farm connections are its workers.** `GET`, `POST` and
8
+ `DELETE /projects/:projectId/render-farm-connections` are now
9
+ `/projects/:projectId/workers`, which takes `{ name, provider?, endpoint, credential?, kinds,
10
+ categories, autoKind?, timeoutSeconds?, description? }`. They're still kept in the project's
11
+ `renderFarmConnections` list.
12
+ - **Jobs are kept in the main database** (a new `jobs` table, created at startup, or collection),
13
+ not in memory: they survive restarts and every instance sees them. `GET /jobs/:jobId` answers
14
+ from there.
15
+ - `canUseBuiltin` may be called with kind `'worker'`.
16
+
17
+ ### Added
18
+
19
+ - **Processing workers**: StreamBy sends jobs to external workers (the host's, in the router
20
+ config's `workers`; or a project's own) and keeps what they make, as a new asset derived from
21
+ the original or as the asset's new version, with the worker's report. The README documents the
22
+ contract a worker follows.
23
+ - `POST /projects/:projectId/jobs/process` asks for one; `GET /projects/:projectId/jobs` lists
24
+ a project's jobs; `POST /projects/:projectId/jobs/:jobId/retry` sends a failed one again.
25
+ - Workers call back to `/worker-jobs/:jobId/{progress,upload-url,complete,fail}` with a token
26
+ of that job's attempt alone.
27
+ - A job not taken, or not finished in time, is sent again (up to `workerOptions.maxAttempts`);
28
+ every instance looks for them (`workerOptions.sweepIntervalMs`), and only one sends each.
29
+ - A worker with an `autoKind` runs by itself on an asset of its categories marked as a
30
+ deliverable, unless it's marked with an API token without `jobs:run`.
31
+ - **Behind a proxy that ends TLS, set `workerOptions.callbackBaseUrl`** to the router's public
32
+ https URL: without it, the callback URL is made from the request (`req.protocol`), and comes
33
+ out as `http://`.
34
+ - A project's worker is reached only over https and never at a local, private, shared,
35
+ benchmarking, multicast or reserved address: neither as its endpoint is written nor as its
36
+ name resolves, checked when it's connected and again right before each job is sent. A job
37
+ for one that resolves to such an address fails for good. `workerOptions.allowLocalEndpoints`
38
+ lifts that, for development. Its credential is used only while whoever connected it is
39
+ still in the project.
40
+ - A worker's answer to a job it doesn't take is logged, not shown: the job says "The worker
41
+ refused the job".
42
+ - A job is sent as whoever asked for it, with the platform role they had then (a host's
43
+ `canUseBuiltin` decides on sending it as on asking for it).
44
+ - MCP tools `list_workers`, `process_asset`, `list_jobs` and `retry_job`.
45
+ - Deleting a project deletes its jobs.
46
+ - Job events (`jobEvent`) carry the job's `projectId` and `status`.
47
+
48
+ ### Fixed
49
+
50
+ - **Instances starting together don't deadlock on StreamBy's tables.** The statements that make
51
+ them (or bring them up to date) at startup run one instance at a time, in a transaction that
52
+ holds a transaction-level advisory lock (so it works behind a pooler in transaction mode, such
53
+ as Supabase's).
54
+
55
+ ## 0.39.0
56
+
57
+ ### Added
58
+
59
+ - **Every resource of a project has its own list**, so a client fetches the one it shows instead
60
+ of the whole project (which still carries them all). New: `GET /projects/:id/exports` (what
61
+ each export is, as the project lists them: not its schema nor its credentials),
62
+ `GET /projects/:id/integrations` and `GET /projects/:id/origins`. They answer `{ data }`, as
63
+ the existing lists do (`connections`, `connections/db`, `connections/storage`, `members`,
64
+ `pipelines`, `workflow`), and are for the project's members. With an API token, the exports
65
+ need `exports:read` and the other two `project:read`.
66
+
3
67
  ## 0.38.0
4
68
 
5
69
  ### Added
package/README.md CHANGED
@@ -201,7 +201,9 @@ A token sees only the tools whose permissions it has.
201
201
  | `create_pipeline`, `update_pipeline` | `workflow:write` |
202
202
  | `list_exports` | `project:read` and `exports:read` |
203
203
  | `get_export` | `exports:read` |
204
- | `get_job` | `jobs:read` |
204
+ | `get_job`, `list_jobs` | `jobs:read` |
205
+ | `list_workers` | `project:read` |
206
+ | `process_asset`, `retry_job` | `jobs:run` |
205
207
  | `list_sequences`, `list_shots`, `list_tasks` | `production:read` |
206
208
  | `update_sequence`, `update_shot`, `update_task` | `production:write` |
207
209
  | `create_sequence`, `create_shot`, `create_task` | `production:write` and `workflow:write` |
@@ -339,6 +341,111 @@ The claude.ai connector needs OAuth, which isn't available yet: it's for API tok
339
341
 
340
342
  ---
341
343
 
344
+ ## ⚙️ Processing workers
345
+
346
+ StreamBy doesn't process assets itself (optimizing a glTF, making LODs, transcoding...): it
347
+ sends the work to a **worker**, an external service, and keeps what it makes. Anyone can write a
348
+ compatible worker by following the contract below.
349
+
350
+ **Where workers come from**
351
+
352
+ - **The host's**, offered to every project, in the router config (each one gated, like the other
353
+ built-ins, by `canUseBuiltin` with kind `'worker'`):
354
+
355
+ ```ts
356
+ createStreamByRouter({
357
+ // ...
358
+ workers: [{
359
+ id: 'gltf', name: 'glTF optimizer',
360
+ endpoint: 'https://worker.example.com/jobs', credential: process.env.GLTF_WORKER_TOKEN,
361
+ kinds: ['gltf-optimize', 'lod'], // what it does
362
+ categories: ['3d-models'], // the assets it takes
363
+ autoKind: 'gltf-optimize', // optional: run on its own (see below)
364
+ timeoutSeconds: 3600, // optional: how long it may take a job
365
+ }],
366
+ workerOptions: {
367
+ callbackBaseUrl: 'https://api.example.com/streamby', // where workers call back (see below)
368
+ maxAttempts: 3, // how many times a job is sent before it fails
369
+ sweepIntervalMs: 30_000, // how often each instance looks for jobs to send again
370
+ },
371
+ });
372
+ ```
373
+
374
+ - **A project's own**, connected from the project (`GET`, `POST` and
375
+ `DELETE /projects/:projectId/workers`; a project's render farms are workers too). Its endpoint
376
+ must be https and never reach a local or private address: not as written, nor as its name
377
+ resolves, which is checked when it's connected and again right before each job is sent (a
378
+ name can point at the server's own network, or start pointing there later). A job for one
379
+ that resolves to such an address fails for good. `workerOptions.allowLocalEndpoints` lifts
380
+ all this, for development. Its credential is stored encrypted, and used only while whoever
381
+ connected it is still in the project.
382
+
383
+ The host's own workers aren't checked this way: the host chose them, and they may well be on
384
+ its private network.
385
+
386
+ `GET /projects/:projectId/workers` lists the workers a member can use.
387
+
388
+ **Asking for a job**
389
+
390
+ `POST /projects/:projectId/jobs/process` with
391
+ `{ assetId, versionId?, worker: { source: 'builtin' | 'project', id }, kind, profile?, resultMode? }`
392
+ (editors and admins; an API token needs `jobs:run`, and the MCP server has `process_asset`).
393
+ `resultMode` decides where the result goes:
394
+
395
+ - `'derived'` (the default): a new asset, linked to the original (`derivedFrom`), which is never
396
+ touched;
397
+ - `'version'`: the asset's new version, at its own key, so its URL serves the result; the
398
+ version it replaces is archived, and can still be downloaded.
399
+
400
+ A worker with an `autoKind` also runs by itself on an asset of its categories when it's marked
401
+ as a deliverable (by a session, or by an API token that has `jobs:run`: one without it marks the
402
+ deliverable and starts nothing).
403
+
404
+ Jobs are kept in the main database (a `jobs` table or collection): they survive restarts, and
405
+ every instance sees them. `GET /projects/:projectId/jobs` lists them (`?assetId=` for one asset's);
406
+ `POST /projects/:projectId/jobs/:jobId/retry` sends a failed one again.
407
+
408
+ **The contract: what a worker receives**
409
+
410
+ StreamBy sends each job with a `POST` to the worker's endpoint
411
+ (`Authorization: Bearer <the worker's credential>`):
412
+
413
+ ```json
414
+ {
415
+ "jobId": "…", "kind": "gltf-optimize", "profile": { "textureFormat": "ktx2" }, "attempt": 1,
416
+ "asset": { "id": "…", "versionId": null, "fileName": "hero.glb", "contentType": "model/gltf-binary", "category": "3d-models" },
417
+ "download": { "url": "https://…signed…", "expiresAt": "…" },
418
+ "callback": { "url": "https://api.example.com/streamby/worker-jobs/<jobId>", "token": "…" },
419
+ "deadline": "…"
420
+ }
421
+ ```
422
+
423
+ The worker answers `2xx` to take it (any other answer, or none, and the job is sent again later),
424
+ downloads the asset with `download.url`, and does its work before `deadline`.
425
+
426
+ `callback.url` is `workerOptions.callbackBaseUrl` plus the job's path. Without it, it's made
427
+ from the request that asked for the job (`req.protocol` and its host): behind a proxy that ends
428
+ TLS (and without Express's `trust proxy`), that comes out as `http://`, which a worker can't call
429
+ back to. Set `callbackBaseUrl` to the router's public https URL.
430
+
431
+ **The contract: what a worker calls back**
432
+
433
+ Every callback is a `POST` to `callback.url` plus an action, with
434
+ `Authorization: Bearer <callback.token>`. The token is the job's for this attempt alone: a
435
+ callback without it, with another, or after the job ended answers 401.
436
+
437
+ | Action | Body | Answer |
438
+ |---|---|---|
439
+ | `/progress` | `{ "progress": 0-100, "stage": "optimizing" }` (either) | the job |
440
+ | `/upload-url` | `{ "fileName": "hero.glb", "contentType": "model/gltf-binary" }` (of the asset's category) | `{ "url", "method": "PUT", "headers", "expiresAt" }`: upload the result there |
441
+ | `/complete` | `{ "report": { … } }` (optional; any JSON object, kept with the result) | the job, completed, with its `result`: `{ assetId, versionId, mode }` |
442
+ | `/fail` | `{ "error": "…", "retryable": false }` | the job, failed with its error (sent again if `retryable` and attempts are left) |
443
+
444
+ A job its worker doesn't complete before its deadline is sent again, up to
445
+ `workerOptions.maxAttempts` times; then it fails, with its error, and can be retried.
446
+
447
+ ---
448
+
342
449
  ## 👥 Project roles
343
450
 
344
451
  What a user can do in a project depends on their role in that project, as an active member
@@ -41,7 +41,28 @@ 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();
51
+ try {
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) => {
45
66
  try {
46
67
  await pool.query(`
47
68
  CREATE SCHEMA IF NOT EXISTS streamby;
@@ -179,6 +200,40 @@ const ensureTablesExist = async (pool) => {
179
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
238
  await pool.query(`
184
239
  CREATE TABLE IF NOT EXISTS streamby.mcp_audit (
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' });
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exportRouter = exportRouter;
4
4
  const express_1 = require("express");
5
5
  const deliverables_1 = require("../../services/deliverables");
6
+ const sanitize_1 = require("../../utils/sanitize");
6
7
  const manager_1 = require("../../models/manager");
7
8
  const auth_1 = require("../../utils/auth");
8
9
  const export_1 = require("../../services/export");
@@ -61,6 +62,21 @@ const DEFAULT_NODE_SCHEMA = {
61
62
  function exportRouter(config) {
62
63
  const router = (0, express_1.Router)();
63
64
  const Project = (0, manager_1.getModel)('projects');
65
+ // ─── A project's exports, on their own ────────────────────────────────────
66
+ router.get('/projects/:id/exports', async (req, res) => {
67
+ try {
68
+ const auth = req.auth;
69
+ const project = await Project.findOne({ _id: req.params.id });
70
+ if (!project)
71
+ return res.status(404).json({ message: 'Project not found' });
72
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
73
+ return res.status(403).json({ message: 'Unauthorized access' });
74
+ return res.status(200).json({ data: (0, sanitize_1.exportSummaries)(project.exports) });
75
+ }
76
+ catch (err) {
77
+ res.status(500).json({ message: 'Failed to fetch exports', details: err.message });
78
+ }
79
+ });
64
80
  router.get('/projects/:id/exports/:export_id', async (req, res) => {
65
81
  try {
66
82
  const auth = await config.authProvider(req);
@@ -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');
@@ -196,15 +197,7 @@ function projectRouter(config) {
196
197
  if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
197
198
  return res.status(403).json({ message: 'Unauthorized project access' });
198
199
  }
199
- const exports = (project.exports || []).map((e) => ({
200
- id: e.id,
201
- name: e.name,
202
- description: e.description,
203
- method: e.method,
204
- private: e.private,
205
- useConnections: e.useConnections,
206
- useCredentials: e.useCredentials,
207
- }));
200
+ const exports = (0, sanitize_1.exportSummaries)(project.exports);
208
201
  const sanitized = (0, sanitize_1.sanitizeProject)({ ...project, id: project._id || project.id, _id: undefined, connections: project.connections || [], exports });
209
202
  // Whether it has production data, for its Production section to show even without a
210
203
  // production node in its workflow.
@@ -262,9 +255,12 @@ function projectRouter(config) {
262
255
  return res.status(403).json({ message: 'Only a project admin can delete it' });
263
256
  }
264
257
  await Project.useDbType(project.dbType).delete({ _id: projectId });
265
- // 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.
266
260
  await (0, apiTokens_1.deleteProjectApiTokens)(String(projectId));
267
261
  await (0, production_1.deleteProductionData)(String(projectId));
262
+ if (projectId)
263
+ await (0, jobStore_1.jobStore)().deleteProject(String(projectId));
268
264
  const allProjects = await Project.find({});
269
265
  const projects = allProjects
270
266
  .filter(project => {
@@ -381,6 +377,21 @@ function projectRouter(config) {
381
377
  res.status(500).json({ message: 'Failed to update project image', details: err.message });
382
378
  }
383
379
  });
380
+ // ─── A project's allowed origins, on their own ────────────────────────────
381
+ router.get('/projects/:id/origins', async (req, res) => {
382
+ try {
383
+ const auth = req.auth;
384
+ const project = await Project.findOne({ _id: req.params.id });
385
+ if (!project)
386
+ return res.status(404).json({ message: 'Project not found' });
387
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
388
+ return res.status(403).json({ message: 'Unauthorized project access' });
389
+ return res.status(200).json({ data: project.allowedOrigin || [] });
390
+ }
391
+ catch (err) {
392
+ res.status(500).json({ message: 'Failed to fetch project origins', details: err.message });
393
+ }
394
+ });
384
395
  router.patch('/projects/:id/origins', async (req, res) => {
385
396
  try {
386
397
  const auth = req.auth;
@@ -403,6 +414,21 @@ function projectRouter(config) {
403
414
  res.status(500).json({ message: 'Failed to update project origins', details: err.message });
404
415
  }
405
416
  });
417
+ // ─── A project's integrations, on their own ───────────────────────────────
418
+ router.get('/projects/:id/integrations', async (req, res) => {
419
+ try {
420
+ const auth = req.auth;
421
+ const project = await Project.findOne({ _id: req.params.id });
422
+ if (!project)
423
+ return res.status(404).json({ message: 'Project not found' });
424
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
425
+ return res.status(403).json({ message: 'Unauthorized project access' });
426
+ return res.status(200).json({ data: (0, sanitize_1.sanitizeIntegrationConnections)(project.integrationConnections) });
427
+ }
428
+ catch (err) {
429
+ res.status(500).json({ message: 'Failed to fetch project integrations', details: err.message });
430
+ }
431
+ });
406
432
  // ─── Add an integration to an existing project ───────────────────────────
407
433
  router.post('/projects/:id/integrations', async (req, res) => {
408
434
  try {