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.
- package/CHANGELOG.md +64 -0
- package/README.md +108 -1
- package/dist/adapters/database/connectionManager.js +55 -0
- package/dist/mcp/tools.js +41 -0
- package/dist/middleware/createRouter.js +11 -1
- package/dist/middleware/routes/ai.js +3 -3
- package/dist/middleware/routes/deliverable.d.ts +4 -2
- package/dist/middleware/routes/deliverable.js +16 -7
- package/dist/middleware/routes/export.js +16 -0
- package/dist/middleware/routes/media.js +7 -6
- package/dist/middleware/routes/project.js +36 -10
- package/dist/middleware/routes/renderFarm.js +4 -77
- package/dist/middleware/routes/storage.js +13 -27
- package/dist/middleware/routes/worker.d.ts +7 -0
- package/dist/middleware/routes/worker.js +125 -0
- package/dist/middleware/routes/workerCallback.d.ts +5 -0
- package/dist/middleware/routes/workerCallback.js +31 -0
- package/dist/services/aiProcessor.js +11 -11
- package/dist/services/apiTokenPermissions.js +8 -7
- package/dist/services/executor.js +10 -10
- package/dist/services/jobQueue.d.ts +6 -4
- package/dist/services/jobQueue.js +28 -16
- package/dist/services/jobStore.d.ts +27 -0
- package/dist/services/jobStore.js +177 -0
- package/dist/services/mediaProcessor.js +16 -16
- package/dist/services/vfxProcessor.js +12 -12
- package/dist/services/workerJobs.d.ts +73 -0
- package/dist/services/workerJobs.js +459 -0
- package/dist/services/workers.d.ts +78 -0
- package/dist/services/workers.js +179 -0
- package/dist/types/index.d.ts +52 -2
- package/dist/utils/assetCategories.d.ts +4 -0
- package/dist/utils/assetCategories.js +21 -0
- package/dist/utils/builtinAccess.d.ts +1 -1
- package/dist/utils/builtinAccess.js +4 -3
- package/dist/utils/privateAddress.d.ts +10 -0
- package/dist/utils/privateAddress.js +85 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +14 -0
- 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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 {
|