streamby-core 0.39.0 → 0.41.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 (63) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +661 -661
  3. package/README.md +785 -573
  4. package/dist/adapters/database/connectionManager.js +182 -124
  5. package/dist/adapters/database/sql.js +1 -1
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.js +9 -1
  8. package/dist/mcp/audit.d.ts +5 -2
  9. package/dist/mcp/audit.js +8 -0
  10. package/dist/mcp/server.js +3 -3
  11. package/dist/mcp/tools.js +41 -0
  12. package/dist/middleware/createRouter.js +14 -1
  13. package/dist/middleware/routes/ai.js +3 -3
  14. package/dist/middleware/routes/deliverable.d.ts +4 -2
  15. package/dist/middleware/routes/deliverable.js +16 -7
  16. package/dist/middleware/routes/mcp.js +11 -4
  17. package/dist/middleware/routes/media.js +7 -6
  18. package/dist/middleware/routes/{apiConnection.d.ts → oauthResource.d.ts} +1 -1
  19. package/dist/middleware/routes/oauthResource.js +18 -0
  20. package/dist/middleware/routes/project.js +5 -1
  21. package/dist/middleware/routes/renderFarm.js +4 -77
  22. package/dist/middleware/routes/storage.js +13 -27
  23. package/dist/middleware/routes/worker.d.ts +7 -0
  24. package/dist/middleware/routes/worker.js +125 -0
  25. package/dist/middleware/routes/workerCallback.d.ts +5 -0
  26. package/dist/middleware/routes/workerCallback.js +31 -0
  27. package/dist/models/Model.js +9 -9
  28. package/dist/services/aiProcessor.js +11 -11
  29. package/dist/services/apiTokenPermissions.js +6 -5
  30. package/dist/services/auth.d.ts +2 -0
  31. package/dist/services/auth.js +36 -10
  32. package/dist/services/executor.js +10 -10
  33. package/dist/services/jobQueue.d.ts +6 -4
  34. package/dist/services/jobQueue.js +28 -16
  35. package/dist/services/jobStore.d.ts +27 -0
  36. package/dist/services/jobStore.js +177 -0
  37. package/dist/services/mediaProcessor.js +16 -16
  38. package/dist/services/oauthTokens.d.ts +60 -0
  39. package/dist/services/oauthTokens.js +164 -0
  40. package/dist/services/vfxProcessor.js +12 -12
  41. package/dist/services/workerJobs.d.ts +73 -0
  42. package/dist/services/workerJobs.js +459 -0
  43. package/dist/services/workers.d.ts +78 -0
  44. package/dist/services/workers.js +179 -0
  45. package/dist/types/index.d.ts +64 -2
  46. package/dist/utils/assetCategories.d.ts +4 -0
  47. package/dist/utils/assetCategories.js +21 -0
  48. package/dist/utils/builtinAccess.d.ts +1 -1
  49. package/dist/utils/builtinAccess.js +4 -3
  50. package/dist/utils/privateAddress.d.ts +10 -0
  51. package/dist/utils/privateAddress.js +85 -0
  52. package/package.json +1 -1
  53. package/dist/middleware/routes/apiConnection.js +0 -100
  54. package/dist/middleware/routes/credential.d.ts +0 -3
  55. package/dist/middleware/routes/credential.js +0 -80
  56. package/dist/services/apiConnection.d.ts +0 -18
  57. package/dist/services/apiConnection.js +0 -65
  58. package/dist/services/credential.d.ts +0 -8
  59. package/dist/services/credential.js +0 -70
  60. package/dist/services/pipeline.d.ts +0 -2
  61. package/dist/services/pipeline.js +0 -292
  62. package/dist/utils/crypto.d.ts +0 -3
  63. package/dist/utils/crypto.js +0 -32
@@ -41,159 +41,217 @@ 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");
252
+ -- An app's calls (an OAuth access token) are recorded by its client, with no token.
253
+ ALTER TABLE streamby.mcp_audit ADD COLUMN IF NOT EXISTS "oauthClientId" VARCHAR(255);
254
+ ALTER TABLE streamby.mcp_audit ALTER COLUMN "tokenId" DROP NOT NULL;
197
255
  `);
198
256
  console.log('✅ "mcp_audit" table ensured to exist.');
199
257
  }
@@ -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/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { createStreamByRouter } from './middleware/createRouter';
2
2
  export { emitToUser } from './services/wsHub';
3
3
  export { createNotification } from './services/notification';
4
- export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission } from './types';
4
+ export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission, OAuthClientInfo, OAuthResourceConfig } from './types';
5
5
  export { resolveApiToken, API_TOKEN_PREFIX } from './services/apiTokens';
6
6
  export { PROJECT_PERMISSIONS, ACCOUNT_PERMISSIONS } from './services/apiTokenPermissions';
7
7
  export type { ResolvedApiToken } from './services/apiTokens';
8
+ export { createOAuthTokenVerifier, looksLikeOAuthAccessToken, protectedResourceMetadata, defaultResourceMetadataUrl, permissionsForOAuthScopes, OAUTH_SCOPES, OAUTH_SCOPE_PERMISSIONS, } from './services/oauthTokens';
9
+ export type { VerifiedOAuthToken, OAuthTokenVerifierOptions, Jwks, Jwk, OAuthScope } from './services/oauthTokens';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
3
+ exports.OAUTH_SCOPE_PERMISSIONS = exports.OAUTH_SCOPES = exports.permissionsForOAuthScopes = exports.defaultResourceMetadataUrl = exports.protectedResourceMetadata = exports.looksLikeOAuthAccessToken = exports.createOAuthTokenVerifier = exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
4
4
  var createRouter_1 = require("./middleware/createRouter");
5
5
  Object.defineProperty(exports, "createStreamByRouter", { enumerable: true, get: function () { return createRouter_1.createStreamByRouter; } });
6
6
  var wsHub_1 = require("./services/wsHub");
@@ -13,3 +13,11 @@ Object.defineProperty(exports, "API_TOKEN_PREFIX", { enumerable: true, get: func
13
13
  var apiTokenPermissions_1 = require("./services/apiTokenPermissions");
14
14
  Object.defineProperty(exports, "PROJECT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.PROJECT_PERMISSIONS; } });
15
15
  Object.defineProperty(exports, "ACCOUNT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.ACCOUNT_PERMISSIONS; } });
16
+ var oauthTokens_1 = require("./services/oauthTokens");
17
+ Object.defineProperty(exports, "createOAuthTokenVerifier", { enumerable: true, get: function () { return oauthTokens_1.createOAuthTokenVerifier; } });
18
+ Object.defineProperty(exports, "looksLikeOAuthAccessToken", { enumerable: true, get: function () { return oauthTokens_1.looksLikeOAuthAccessToken; } });
19
+ Object.defineProperty(exports, "protectedResourceMetadata", { enumerable: true, get: function () { return oauthTokens_1.protectedResourceMetadata; } });
20
+ Object.defineProperty(exports, "defaultResourceMetadataUrl", { enumerable: true, get: function () { return oauthTokens_1.defaultResourceMetadataUrl; } });
21
+ Object.defineProperty(exports, "permissionsForOAuthScopes", { enumerable: true, get: function () { return oauthTokens_1.permissionsForOAuthScopes; } });
22
+ Object.defineProperty(exports, "OAUTH_SCOPES", { enumerable: true, get: function () { return oauthTokens_1.OAUTH_SCOPES; } });
23
+ Object.defineProperty(exports, "OAUTH_SCOPE_PERMISSIONS", { enumerable: true, get: function () { return oauthTokens_1.OAUTH_SCOPE_PERMISSIONS; } });
@@ -1,7 +1,8 @@
1
- import { StreamByConfig } from '../types';
1
+ import { Auth, StreamByConfig } from '../types';
2
2
  export interface McpAuditEntry {
3
3
  userId: string;
4
- tokenId: string;
4
+ tokenId: string | null;
5
+ oauthClientId: string | null;
5
6
  projectId: string | null;
6
7
  tool: string;
7
8
  ok: boolean;
@@ -9,4 +10,6 @@ export interface McpAuditEntry {
9
10
  error: string | null;
10
11
  durationMs: number;
11
12
  }
13
+ /** Who made an MCP request, as the audit log keeps it. */
14
+ export declare const auditCallerOf: (auth: Auth) => Pick<McpAuditEntry, "userId" | "tokenId" | "oauthClientId">;
12
15
  export declare function recordMcpCall(entry: McpAuditEntry, config: StreamByConfig): Promise<void>;
package/dist/mcp/audit.js CHANGED
@@ -3,9 +3,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.auditCallerOf = void 0;
6
7
  exports.recordMcpCall = recordMcpCall;
7
8
  const crypto_1 = __importDefault(require("crypto"));
8
9
  const manager_1 = require("../models/manager");
10
+ /** Who made an MCP request, as the audit log keeps it. */
11
+ const auditCallerOf = (auth) => ({
12
+ userId: auth.userId,
13
+ tokenId: auth.oauth ? null : auth.apiToken?.tokenId ?? null,
14
+ oauthClientId: auth.oauth?.clientId ?? null,
15
+ });
16
+ exports.auditCallerOf = auditCallerOf;
9
17
  async function recordMcpCall(entry, config) {
10
18
  try {
11
19
  const dbType = config.databases?.find(db => db.main)?.type ?? 'nosql';
@@ -40,7 +40,7 @@ function createMcpServer(auth, api, config, version) {
40
40
  const refused = await (0, workflowNodes_1.ensureWorkflowNode)(tool.workflowNode, tool.projectOf?.(args), auth, api);
41
41
  if (refused) {
42
42
  await (0, audit_1.recordMcpCall)({
43
- userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
43
+ ...(0, audit_1.auditCallerOf)(auth), projectId, tool: tool.name,
44
44
  ok: false, status: null, error: refused, durationMs: Date.now() - started,
45
45
  }, config);
46
46
  return { isError: true, content: [{ type: 'text', text: refused }] };
@@ -50,7 +50,7 @@ function createMcpServer(auth, api, config, version) {
50
50
  status = answer.status;
51
51
  const ok = status >= 200 && status < 300;
52
52
  await (0, audit_1.recordMcpCall)({
53
- userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
53
+ ...(0, audit_1.auditCallerOf)(auth), projectId, tool: tool.name,
54
54
  ok, status, error: ok ? null : errorText(answer.body, status), durationMs: Date.now() - started,
55
55
  }, config);
56
56
  if (!ok)
@@ -61,7 +61,7 @@ function createMcpServer(auth, api, config, version) {
61
61
  catch (err) {
62
62
  const message = err instanceof Error ? err.message : String(err);
63
63
  await (0, audit_1.recordMcpCall)({
64
- userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
64
+ ...(0, audit_1.auditCallerOf)(auth), projectId, tool: tool.name,
65
65
  ok: false, status, error: message, durationMs: Date.now() - started,
66
66
  }, config);
67
67
  return { isError: true, content: [{ type: 'text', text: `The call failed: ${message}` }] };
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");
@@ -27,6 +28,7 @@ const dbConnection_1 = require("./routes/dbConnection");
27
28
  const storageConnection_1 = require("./routes/storageConnection");
28
29
  const userIntegration_1 = require("./routes/userIntegration");
29
30
  const oauth_1 = require("./routes/oauth");
31
+ const oauthResource_1 = require("./routes/oauthResource");
30
32
  const notification_1 = require("./routes/notification");
31
33
  const subscription_1 = require("./routes/subscription");
32
34
  const media_1 = require("./routes/media");
@@ -38,11 +40,15 @@ const production_1 = require("./routes/production");
38
40
  const rights_1 = require("./routes/rights");
39
41
  const workflow_1 = require("./routes/workflow");
40
42
  const pipeline_1 = require("./routes/pipeline");
43
+ const worker_1 = require("./routes/worker");
44
+ const workerCallback_1 = require("./routes/workerCallback");
45
+ const workerJobs_1 = require("../services/workerJobs");
41
46
  const deliverable_1 = require("./routes/deliverable");
42
47
  function createStreamByRouter(config) {
43
48
  const router = express_1.default.Router();
44
49
  if (config.encrypt)
45
50
  (0, encryption_1.setEncryptionKey)(config.encrypt, config.encryptPreviousKeys);
51
+ (0, jobStore_1.setJobStoreDatabase)(config);
46
52
  // Establish DB connections asynchronously (Postgres/Mongo + schema migrations).
47
53
  // Until they're ready, HOLD incoming requests instead of letting them through —
48
54
  // route handlers resolve projects/exports (e.g. the /menu-list export) against a
@@ -69,6 +75,8 @@ function createStreamByRouter(config) {
69
75
  })
70
76
  .finally(() => {
71
77
  ready = true;
78
+ // Jobs left pending (or with their worker's time up) by a restart are sent again.
79
+ (0, workerJobs_1.startWorkerSweeper)({ ...config, adapter });
72
80
  });
73
81
  router.use((_req, _res, next) => {
74
82
  if (ready)
@@ -105,9 +113,13 @@ function createStreamByRouter(config) {
105
113
  if (config.websocket?.server) {
106
114
  (0, wsHub_1.initWsHub)(config.websocket.server, config);
107
115
  }
116
+ // Where to sign in for a token (RFC 9728): open, before anything asks for one.
117
+ router.use((0, oauthResource_1.oauthResourceRouter)(config));
108
118
  // /auth answers whether there's a session (it reads it itself); everything after needs one,
109
119
  // but for the public export route (see authenticate).
110
120
  router.use((0, auth_2.authRouter)(config));
121
+ // Workers call back with their job's token, not a session (see routes/workerCallback).
122
+ router.use((0, workerCallback_1.workerCallbackRouter)({ ...config, adapter }));
111
123
  router.use((0, auth_1.authenticate)(config));
112
124
  router.use(auth_1.enforceApiTokenScope);
113
125
  router.use(subscription_1.ensureSubscription);
@@ -135,6 +147,7 @@ function createStreamByRouter(config) {
135
147
  router.use((0, rights_1.rightsRouter)(config));
136
148
  router.use((0, workflow_1.workflowRouter)(config));
137
149
  router.use((0, pipeline_1.pipelineRouter)(config));
138
- router.use((0, deliverable_1.deliverableRouter)(config));
150
+ router.use((0, worker_1.workerRouter)({ ...config, adapter }));
151
+ router.use((0, deliverable_1.deliverableRouter)({ ...config, adapter }));
139
152
  return router;
140
153
  }
@@ -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;