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.
- package/CHANGELOG.md +84 -0
- package/LICENSE +661 -661
- package/README.md +785 -573
- package/dist/adapters/database/connectionManager.js +182 -124
- package/dist/adapters/database/sql.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +9 -1
- package/dist/mcp/audit.d.ts +5 -2
- package/dist/mcp/audit.js +8 -0
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools.js +41 -0
- package/dist/middleware/createRouter.js +14 -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/mcp.js +11 -4
- package/dist/middleware/routes/media.js +7 -6
- package/dist/middleware/routes/{apiConnection.d.ts → oauthResource.d.ts} +1 -1
- package/dist/middleware/routes/oauthResource.js +18 -0
- package/dist/middleware/routes/project.js +5 -1
- 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/models/Model.js +9 -9
- package/dist/services/aiProcessor.js +11 -11
- package/dist/services/apiTokenPermissions.js +6 -5
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +36 -10
- 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/oauthTokens.d.ts +60 -0
- package/dist/services/oauthTokens.js +164 -0
- 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 +64 -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/package.json +1 -1
- package/dist/middleware/routes/apiConnection.js +0 -100
- package/dist/middleware/routes/credential.d.ts +0 -3
- package/dist/middleware/routes/credential.js +0 -80
- package/dist/services/apiConnection.d.ts +0 -18
- package/dist/services/apiConnection.js +0 -65
- package/dist/services/credential.d.ts +0 -8
- package/dist/services/credential.js +0 -70
- package/dist/services/pipeline.d.ts +0 -2
- package/dist/services/pipeline.js +0 -292
- package/dist/utils/crypto.d.ts +0 -3
- package/dist/utils/crypto.js +0 -32
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.summarizeProjectWorker = exports.projectWorkers = exports.PrivateEndpointError = exports.WorkerError = exports.DEFAULT_WORKER_TIMEOUT_SECONDS = exports.WORKER_PROVIDERS = exports.STORAGE_CATEGORIES = void 0;
|
|
7
|
+
exports.availableWorkers = availableWorkers;
|
|
8
|
+
exports.resolveWorker = resolveWorker;
|
|
9
|
+
exports.checkWorkerEndpoint = checkWorkerEndpoint;
|
|
10
|
+
exports.assertPublicEndpoint = assertPublicEndpoint;
|
|
11
|
+
exports.newWorkerConnection = newWorkerConnection;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const auth_1 = require("../utils/auth");
|
|
14
|
+
const builtinAccess_1 = require("../utils/builtinAccess");
|
|
15
|
+
const encryption_1 = require("../utils/encryption");
|
|
16
|
+
const privateAddress_1 = require("../utils/privateAddress");
|
|
17
|
+
// Processing workers: external services StreamBy sends jobs to (see workerJobs). A project
|
|
18
|
+
// can use the host's built-in ones (StreamByConfig.workers, gated by canUseBuiltin) and its
|
|
19
|
+
// own, connected from the project. A project's workers are kept in its
|
|
20
|
+
// `renderFarmConnections` list (render farms are workers too); each one's credential is
|
|
21
|
+
// encrypted, and it's used only while whoever connected it is still in the project.
|
|
22
|
+
exports.STORAGE_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
23
|
+
exports.WORKER_PROVIDERS = ['streamby-worker', 'flamenco', 'deadline', 'rebusfarm', 'sheepit', 'custom'];
|
|
24
|
+
exports.DEFAULT_WORKER_TIMEOUT_SECONDS = 3600;
|
|
25
|
+
class WorkerError extends Error {
|
|
26
|
+
constructor(message, status = 400) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.WorkerError = WorkerError;
|
|
32
|
+
/** A worker endpoint at a local or private address: never sent to, whatever the attempt. */
|
|
33
|
+
class PrivateEndpointError extends WorkerError {
|
|
34
|
+
}
|
|
35
|
+
exports.PrivateEndpointError = PrivateEndpointError;
|
|
36
|
+
const listOf = (value) => (Array.isArray(value) ? value.filter((v) => typeof v === 'string') : []);
|
|
37
|
+
const projectWorkers = (project) => Array.isArray(project?.renderFarmConnections) ? project.renderFarmConnections : [];
|
|
38
|
+
exports.projectWorkers = projectWorkers;
|
|
39
|
+
const builtinSummary = (worker) => ({
|
|
40
|
+
source: 'builtin', id: worker.id, name: worker.name, provider: 'streamby-worker',
|
|
41
|
+
kinds: worker.kinds ?? [], categories: worker.categories ?? [], autoKind: worker.autoKind ?? null,
|
|
42
|
+
description: null, hasCredential: !!worker.credential, createdBy: null,
|
|
43
|
+
});
|
|
44
|
+
const projectSummary = (worker) => ({
|
|
45
|
+
source: 'project', id: worker.id, name: worker.name, provider: worker.provider,
|
|
46
|
+
kinds: listOf(worker.kinds), categories: listOf(worker.categories),
|
|
47
|
+
autoKind: worker.autoKind ?? null, description: worker.description ?? null,
|
|
48
|
+
hasCredential: !!worker.encryptedCredential, createdBy: worker.createdBy ?? null,
|
|
49
|
+
});
|
|
50
|
+
/** The workers a project's member can send jobs to: the project's own, and the host's they may use. */
|
|
51
|
+
async function availableWorkers(project, auth, config) {
|
|
52
|
+
const builtins = [];
|
|
53
|
+
for (const worker of config.workers ?? []) {
|
|
54
|
+
if (await (0, builtinAccess_1.assertBuiltinAccess)(auth, worker.id, config, 'worker'))
|
|
55
|
+
builtins.push(builtinSummary(worker));
|
|
56
|
+
}
|
|
57
|
+
return [...builtins, ...(0, exports.projectWorkers)(project).map(projectSummary)];
|
|
58
|
+
}
|
|
59
|
+
/** A worker of the project, or of the host, ready to be sent a job as `auth`. */
|
|
60
|
+
async function resolveWorker(project, ref, auth, config) {
|
|
61
|
+
if (ref.source === 'builtin') {
|
|
62
|
+
const worker = (config.workers ?? []).find(w => w.id === ref.id);
|
|
63
|
+
if (!worker)
|
|
64
|
+
throw new WorkerError('Worker not found', 404);
|
|
65
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, worker.id, config, 'worker')))
|
|
66
|
+
throw new WorkerError('This worker is not available to you', 403);
|
|
67
|
+
return {
|
|
68
|
+
ref: { source: 'builtin', id: worker.id, name: worker.name },
|
|
69
|
+
endpoint: worker.endpoint, credential: worker.credential ?? null,
|
|
70
|
+
kinds: worker.kinds ?? [], categories: worker.categories ?? [], autoKind: worker.autoKind ?? null,
|
|
71
|
+
timeoutSeconds: worker.timeoutSeconds ?? exports.DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (ref.source === 'project') {
|
|
75
|
+
const worker = (0, exports.projectWorkers)(project).find(w => w.id === ref.id);
|
|
76
|
+
if (!worker)
|
|
77
|
+
throw new WorkerError('Worker not found', 404);
|
|
78
|
+
// Tied to whoever connected it: once they leave the project, its credential isn't used.
|
|
79
|
+
if (worker.createdBy && !(0, auth_1.activeMemberOf)(project, worker.createdBy)) {
|
|
80
|
+
throw new WorkerError('Whoever connected this worker is no longer in the project: connect it again', 409);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ref: { source: 'project', id: worker.id, name: worker.name },
|
|
84
|
+
endpoint: worker.apiUrl, credential: worker.encryptedCredential ? (0, encryption_1.decrypt)(worker.encryptedCredential) : null,
|
|
85
|
+
kinds: listOf(worker.kinds), categories: listOf(worker.categories),
|
|
86
|
+
autoKind: worker.autoKind ?? null, timeoutSeconds: worker.timeoutSeconds ?? exports.DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
throw new WorkerError("A worker's source is 'builtin' or 'project'");
|
|
90
|
+
}
|
|
91
|
+
// A project's worker is reached from StreamBy's server: only over https, and never at a local
|
|
92
|
+
// or private address (unless the host allows it, for development). The address a name
|
|
93
|
+
// resolves to is checked too, when it's connected and right before each job is sent.
|
|
94
|
+
function checkWorkerEndpoint(value, config) {
|
|
95
|
+
let url;
|
|
96
|
+
try {
|
|
97
|
+
url = new URL(String(value));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new WorkerError('endpoint must be a URL');
|
|
101
|
+
}
|
|
102
|
+
const local = config.workerOptions?.allowLocalEndpoints === true;
|
|
103
|
+
if (url.username || url.password)
|
|
104
|
+
throw new WorkerError("endpoint can't carry credentials: give them as credential");
|
|
105
|
+
if (!local && url.protocol !== 'https:')
|
|
106
|
+
throw new WorkerError('endpoint must use https');
|
|
107
|
+
if (!local && (0, privateAddress_1.isPrivateHostName)(url.hostname))
|
|
108
|
+
throw new WorkerError("endpoint can't be a local or private address");
|
|
109
|
+
if (!['https:', 'http:'].includes(url.protocol))
|
|
110
|
+
throw new WorkerError('endpoint must use https');
|
|
111
|
+
return url.toString();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Throws if a project's worker endpoint resolves, now, to a local or private address (a
|
|
115
|
+
* public-looking name pointing at the server's own network).
|
|
116
|
+
*/
|
|
117
|
+
async function assertPublicEndpoint(endpoint, config) {
|
|
118
|
+
if (config.workerOptions?.allowLocalEndpoints === true)
|
|
119
|
+
return;
|
|
120
|
+
let host;
|
|
121
|
+
try {
|
|
122
|
+
host = new URL(endpoint).hostname;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new WorkerError('endpoint must be a URL');
|
|
126
|
+
}
|
|
127
|
+
let isPrivate;
|
|
128
|
+
try {
|
|
129
|
+
isPrivate = await (0, privateAddress_1.resolvesToPrivateAddress)(host);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new WorkerError(`The worker's host (${host}) couldn't be resolved`, 400);
|
|
133
|
+
}
|
|
134
|
+
if (isPrivate)
|
|
135
|
+
throw new PrivateEndpointError("The worker's endpoint resolves to a local or private address", 400);
|
|
136
|
+
}
|
|
137
|
+
/** A project's new worker, checked and with its credential encrypted. */
|
|
138
|
+
function newWorkerConnection(input, projectId, userId, config) {
|
|
139
|
+
const name = typeof input.name === 'string' ? input.name.trim() : '';
|
|
140
|
+
if (!name)
|
|
141
|
+
throw new WorkerError('A worker needs a name');
|
|
142
|
+
const provider = input.provider === undefined ? 'streamby-worker' : input.provider;
|
|
143
|
+
if (!exports.WORKER_PROVIDERS.includes(provider))
|
|
144
|
+
throw new WorkerError(`provider must be one of ${exports.WORKER_PROVIDERS.join(', ')}`);
|
|
145
|
+
const endpoint = checkWorkerEndpoint(input.endpoint, config);
|
|
146
|
+
const kinds = listOf(input.kinds).map(k => k.trim()).filter(Boolean);
|
|
147
|
+
if (!kinds.length || kinds.length !== listOf(input.kinds).length)
|
|
148
|
+
throw new WorkerError('kinds must list what the worker does (e.g. gltf-optimize)');
|
|
149
|
+
const categories = listOf(input.categories);
|
|
150
|
+
if (!categories.length || !categories.every(c => exports.STORAGE_CATEGORIES.includes(c))) {
|
|
151
|
+
throw new WorkerError(`categories must list asset categories it takes: ${exports.STORAGE_CATEGORIES.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
const autoKind = input.autoKind === undefined || input.autoKind === null || input.autoKind === '' ? null : input.autoKind;
|
|
154
|
+
if (autoKind !== null && (typeof autoKind !== 'string' || !kinds.includes(autoKind)))
|
|
155
|
+
throw new WorkerError('autoKind must be one of its kinds, or null');
|
|
156
|
+
const timeoutSeconds = input.timeoutSeconds === undefined ? undefined : Number(input.timeoutSeconds);
|
|
157
|
+
if (timeoutSeconds !== undefined && !(Number.isInteger(timeoutSeconds) && timeoutSeconds >= 60 && timeoutSeconds <= 7 * 86400)) {
|
|
158
|
+
throw new WorkerError('timeoutSeconds must be a whole number of seconds, from 60 to a week');
|
|
159
|
+
}
|
|
160
|
+
const credential = typeof input.credential === 'string' && input.credential ? input.credential : null;
|
|
161
|
+
if (credential && !(0, encryption_1.isEncryptionKeySet)())
|
|
162
|
+
throw new WorkerError('Credentials cannot be stored: no encryption key is set', 500);
|
|
163
|
+
return {
|
|
164
|
+
id: crypto_1.default.randomUUID(),
|
|
165
|
+
projectId,
|
|
166
|
+
name,
|
|
167
|
+
provider: provider,
|
|
168
|
+
apiUrl: endpoint,
|
|
169
|
+
...(credential ? { encryptedCredential: (0, encryption_1.encrypt)(credential) } : {}),
|
|
170
|
+
kinds: [...new Set(kinds)],
|
|
171
|
+
categories: [...new Set(categories)],
|
|
172
|
+
autoKind: autoKind,
|
|
173
|
+
...(timeoutSeconds !== undefined ? { timeoutSeconds } : {}),
|
|
174
|
+
...(typeof input.description === 'string' && input.description.trim() ? { description: input.description.trim() } : {}),
|
|
175
|
+
createdBy: userId,
|
|
176
|
+
createdAt: new Date(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
exports.summarizeProjectWorker = projectSummary;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -146,10 +146,18 @@ export interface StreamByConfig {
|
|
|
146
146
|
download?: number;
|
|
147
147
|
};
|
|
148
148
|
};
|
|
149
|
+
oauth?: OAuthResourceConfig;
|
|
149
150
|
websocket?: {
|
|
150
151
|
server: WebSocketServer;
|
|
151
152
|
};
|
|
152
|
-
canUseBuiltin?: (auth: Auth, builtinId: string, kind: 'database' | 'storage') => boolean | Promise<boolean>;
|
|
153
|
+
canUseBuiltin?: (auth: Auth, builtinId: string, kind: 'database' | 'storage' | 'worker') => boolean | Promise<boolean>;
|
|
154
|
+
workers?: WorkerConfig[];
|
|
155
|
+
workerOptions?: {
|
|
156
|
+
callbackBaseUrl?: string;
|
|
157
|
+
maxAttempts?: number;
|
|
158
|
+
sweepIntervalMs?: number;
|
|
159
|
+
allowLocalEndpoints?: boolean;
|
|
160
|
+
};
|
|
153
161
|
oauthProviders?: {
|
|
154
162
|
jira?: OAuthClientConfig;
|
|
155
163
|
google?: OAuthClientConfig;
|
|
@@ -172,6 +180,17 @@ export interface Auth {
|
|
|
172
180
|
username: string;
|
|
173
181
|
role: 'viewer' | 'editor' | 'admin';
|
|
174
182
|
apiToken?: ApiTokenScope;
|
|
183
|
+
oauth?: OAuthClientInfo;
|
|
184
|
+
}
|
|
185
|
+
export interface OAuthClientInfo {
|
|
186
|
+
clientId: string;
|
|
187
|
+
clientName?: string;
|
|
188
|
+
}
|
|
189
|
+
export interface OAuthResourceConfig {
|
|
190
|
+
issuer: string;
|
|
191
|
+
resource: string;
|
|
192
|
+
resourceMetadataUrl?: string;
|
|
193
|
+
resourceName?: string;
|
|
175
194
|
}
|
|
176
195
|
export type { ApiTokenPermission, ProjectPermission, AccountPermission };
|
|
177
196
|
export type ApiTokenKind = 'user' | 'project';
|
|
@@ -320,7 +339,7 @@ export interface QcReportRecord {
|
|
|
320
339
|
overallPassed: boolean;
|
|
321
340
|
generatedAt: Date;
|
|
322
341
|
}
|
|
323
|
-
export type JobType = 'ingest' | 'transcode' | 'caption' | 'thumbnail' | 'render' | 'format-convert' | 'lod' | 'distribute' | 'qc' | 'transcription' | 'upscale' | 'generate-asset';
|
|
342
|
+
export type JobType = 'ingest' | 'transcode' | 'caption' | 'thumbnail' | 'render' | 'format-convert' | 'lod' | 'distribute' | 'qc' | 'transcription' | 'upscale' | 'generate-asset' | 'process';
|
|
324
343
|
export type AiProvider = 'openai' | 'deepgram' | 'assemblyai' | 'stabilityai' | 'meshy' | 'elevenlabs' | 'custom';
|
|
325
344
|
export interface PipelineSuggestion {
|
|
326
345
|
rationale: string;
|
|
@@ -439,7 +458,50 @@ export interface JobRecord {
|
|
|
439
458
|
error?: string;
|
|
440
459
|
createdAt: Date;
|
|
441
460
|
updatedAt: Date;
|
|
461
|
+
worker?: WorkerRef | null;
|
|
462
|
+
kind?: string | null;
|
|
463
|
+
profile?: Record<string, unknown> | null;
|
|
464
|
+
versionId?: string | null;
|
|
465
|
+
resultMode?: WorkerResultMode | null;
|
|
466
|
+
attempts?: number;
|
|
467
|
+
maxAttempts?: number;
|
|
468
|
+
nextAttemptAt?: Date | null;
|
|
469
|
+
leaseUntil?: Date | null;
|
|
470
|
+
callbackHash?: string | null;
|
|
471
|
+
upload?: {
|
|
472
|
+
storageKey: string;
|
|
473
|
+
fileName: string;
|
|
474
|
+
contentType: string;
|
|
475
|
+
} | null;
|
|
476
|
+
report?: Record<string, unknown> | null;
|
|
477
|
+
result?: {
|
|
478
|
+
assetId: string;
|
|
479
|
+
versionId: string;
|
|
480
|
+
mode: WorkerResultMode;
|
|
481
|
+
} | null;
|
|
482
|
+
}
|
|
483
|
+
export type WorkerResultMode = 'version' | 'derived';
|
|
484
|
+
export type WorkerSource = 'builtin' | 'project';
|
|
485
|
+
export interface WorkerRef {
|
|
486
|
+
source: WorkerSource;
|
|
487
|
+
id: string;
|
|
488
|
+
name: string;
|
|
442
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* A processing worker a host offers every project (`StreamByConfig.workers`). StreamBy sends
|
|
492
|
+
* it jobs (see the README's worker contract) and keeps what it makes.
|
|
493
|
+
*/
|
|
494
|
+
export interface WorkerConfig {
|
|
495
|
+
id: string;
|
|
496
|
+
name: string;
|
|
497
|
+
endpoint: string;
|
|
498
|
+
credential?: string;
|
|
499
|
+
kinds: string[];
|
|
500
|
+
categories: StorageCategoryName[];
|
|
501
|
+
autoKind?: string | null;
|
|
502
|
+
timeoutSeconds?: number;
|
|
503
|
+
}
|
|
504
|
+
export type StorageCategoryName = 'images' | 'audios' | 'videos' | '3d-models';
|
|
443
505
|
export interface AssetMetadataRecord {
|
|
444
506
|
assetId: string;
|
|
445
507
|
projectId: string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type StorageCategory = 'images' | 'audios' | 'videos' | '3d-models';
|
|
2
|
+
export declare const VALID_CATEGORIES: StorageCategory[];
|
|
3
|
+
/** Whether a file (its content type, or for 3D models its extension) belongs in a category. */
|
|
4
|
+
export declare function validateContentType(contentType: string, filename: string, category: StorageCategory): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The categories assets are kept in, and what each one takes.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.VALID_CATEGORIES = void 0;
|
|
5
|
+
exports.validateContentType = validateContentType;
|
|
6
|
+
exports.VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
7
|
+
const VALID_3D_EXTENSIONS = new Set(['.glb', '.gltf', '.obj', '.fbx', '.stl', '.ply']);
|
|
8
|
+
/** Whether a file (its content type, or for 3D models its extension) belongs in a category. */
|
|
9
|
+
function validateContentType(contentType, filename, category) {
|
|
10
|
+
if (category === 'images')
|
|
11
|
+
return contentType.startsWith('image/');
|
|
12
|
+
if (category === 'audios')
|
|
13
|
+
return contentType.startsWith('audio/');
|
|
14
|
+
if (category === 'videos')
|
|
15
|
+
return contentType.startsWith('video/');
|
|
16
|
+
if (category === '3d-models') {
|
|
17
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
18
|
+
return ext ? VALID_3D_EXTENSIONS.has(`.${ext}`) : false;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Auth, StreamByConfig } from '../types';
|
|
2
|
-
export declare function assertBuiltinAccess(auth: Auth, builtinId: string, config: StreamByConfig, kind: 'database' | 'storage'): Promise<boolean>;
|
|
2
|
+
export declare function assertBuiltinAccess(auth: Auth, builtinId: string, config: StreamByConfig, kind: 'database' | 'storage' | 'worker'): Promise<boolean>;
|
|
3
3
|
export declare function isBuiltinStorageId(id: string, config: StreamByConfig): boolean;
|
|
4
4
|
export declare function isBuiltinDb(id: string, config: StreamByConfig): boolean;
|
|
5
5
|
export declare const BUILTIN_STORAGE_DISPLAY: Record<string, string>;
|
|
@@ -4,9 +4,10 @@ exports.BUILTIN_DB_DISPLAY = exports.BUILTIN_STORAGE_DISPLAY = void 0;
|
|
|
4
4
|
exports.assertBuiltinAccess = assertBuiltinAccess;
|
|
5
5
|
exports.isBuiltinStorageId = isBuiltinStorageId;
|
|
6
6
|
exports.isBuiltinDb = isBuiltinDb;
|
|
7
|
-
// Gates access to a built-in database
|
|
8
|
-
// when the mounting app implements it (e.g. Nhexa-API checking
|
|
9
|
-
// defaults to allow when absent, preserving pre-BYOC behavior for deploys
|
|
7
|
+
// Gates access to a built-in database, storage provider or worker. Delegates to
|
|
8
|
+
// config.canUseBuiltin when the mounting app implements it (e.g. Nhexa-API checking
|
|
9
|
+
// user_subscriptions); defaults to allow when absent, preserving pre-BYOC behavior for deploys
|
|
10
|
+
// without gating.
|
|
10
11
|
async function assertBuiltinAccess(auth, builtinId, config, kind) {
|
|
11
12
|
if (!config.canUseBuiltin)
|
|
12
13
|
return true;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Whether an IP address (v4 or v6) is a local, private or otherwise non-public one. */
|
|
2
|
+
export declare function isPrivateIp(ip: string): boolean;
|
|
3
|
+
/** Whether a URL's host, as written, is a local name or a non-public IP address. */
|
|
4
|
+
export declare function isPrivateHostName(hostname: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Whether a host, resolved now, reaches a non-public address: a name can resolve to one even
|
|
7
|
+
* if it doesn't look like it, and resolve differently later, so it's checked right before each
|
|
8
|
+
* request.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolvesToPrivateAddress(hostname: string): Promise<boolean>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPrivateIp = isPrivateIp;
|
|
4
|
+
exports.isPrivateHostName = isPrivateHostName;
|
|
5
|
+
exports.resolvesToPrivateAddress = resolvesToPrivateAddress;
|
|
6
|
+
const dns_1 = require("dns");
|
|
7
|
+
const net_1 = require("net");
|
|
8
|
+
// Whether an address is one StreamBy's server must not be made to reach on someone's behalf: a
|
|
9
|
+
// loopback, private, link-local, shared, benchmarking, multicast or reserved one (anything a
|
|
10
|
+
// request from the server could use to reach its own network).
|
|
11
|
+
const ipv4Parts = (ip) => ip.split('.').map(Number);
|
|
12
|
+
function isPrivateIpv4(ip) {
|
|
13
|
+
const [a, b] = ipv4Parts(ip);
|
|
14
|
+
return a === 0 // 0.0.0.0/8 ("this" network)
|
|
15
|
+
|| a === 10 // 10.0.0.0/8
|
|
16
|
+
|| a === 127 // 127.0.0.0/8 loopback
|
|
17
|
+
|| (a === 100 && b >= 64 && b <= 127) // 100.64.0.0/10 shared (CGNAT)
|
|
18
|
+
|| (a === 169 && b === 254) // 169.254.0.0/16 link-local (cloud metadata)
|
|
19
|
+
|| (a === 172 && b >= 16 && b <= 31) // 172.16.0.0/12
|
|
20
|
+
|| (a === 192 && b === 0 && ipv4Parts(ip)[2] === 0) // 192.0.0.0/24 protocol assignments
|
|
21
|
+
|| (a === 192 && b === 168) // 192.168.0.0/16
|
|
22
|
+
|| (a === 198 && (b === 18 || b === 19)) // 198.18.0.0/15 benchmarking
|
|
23
|
+
|| a >= 224; // 224.0.0.0/4 multicast, and 240.0.0.0/4 reserved
|
|
24
|
+
}
|
|
25
|
+
// An IPv6 address's 8 groups, expanded (an embedded IPv4 written as dotted quad included).
|
|
26
|
+
function ipv6Groups(ip) {
|
|
27
|
+
let text = ip.toLowerCase().replace(/%.*$/, '');
|
|
28
|
+
const dotted = text.match(/(\d+\.\d+\.\d+\.\d+)$/);
|
|
29
|
+
if (dotted) {
|
|
30
|
+
const [a, b, c, d] = ipv4Parts(dotted[1]);
|
|
31
|
+
text = text.slice(0, -dotted[1].length) + `${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
|
|
32
|
+
}
|
|
33
|
+
const [head, tail] = text.split('::');
|
|
34
|
+
const left = head ? head.split(':') : [];
|
|
35
|
+
const right = tail !== undefined && tail ? tail.split(':') : [];
|
|
36
|
+
const missing = 8 - left.length - right.length;
|
|
37
|
+
if (tail === undefined ? left.length !== 8 : missing < 0)
|
|
38
|
+
return null;
|
|
39
|
+
return [...left, ...Array(tail === undefined ? 0 : missing).fill('0'), ...right].map(group => parseInt(group, 16));
|
|
40
|
+
}
|
|
41
|
+
function isPrivateIpv6(ip) {
|
|
42
|
+
const groups = ipv6Groups(ip);
|
|
43
|
+
if (!groups || groups.some(Number.isNaN))
|
|
44
|
+
return true; // unreadable: not reached
|
|
45
|
+
if (groups.every(g => g === 0))
|
|
46
|
+
return true; // :: unspecified
|
|
47
|
+
if (groups.slice(0, 7).every(g => g === 0) && groups[7] === 1)
|
|
48
|
+
return true; // ::1 loopback
|
|
49
|
+
// ::ffff:a.b.c.d (IPv4-mapped): judged as the IPv4 address it carries.
|
|
50
|
+
if (groups.slice(0, 5).every(g => g === 0) && groups[5] === 0xffff) {
|
|
51
|
+
return isPrivateIpv4(`${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`);
|
|
52
|
+
}
|
|
53
|
+
return (groups[0] & 0xfe00) === 0xfc00 // fc00::/7 unique local (e.g. fdaa::, Fly's network)
|
|
54
|
+
|| (groups[0] & 0xffc0) === 0xfe80 // fe80::/10 link-local
|
|
55
|
+
|| (groups[0] & 0xff00) === 0xff00; // ff00::/8 multicast
|
|
56
|
+
}
|
|
57
|
+
/** Whether an IP address (v4 or v6) is a local, private or otherwise non-public one. */
|
|
58
|
+
function isPrivateIp(ip) {
|
|
59
|
+
const bare = ip.replace(/^\[|\]$/g, '');
|
|
60
|
+
if ((0, net_1.isIP)(bare) === 4)
|
|
61
|
+
return isPrivateIpv4(bare);
|
|
62
|
+
if ((0, net_1.isIP)(bare) === 6)
|
|
63
|
+
return isPrivateIpv6(bare);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const LOCAL_NAME = /^(localhost|.*\.localhost|.*\.local|.*\.internal)$/i;
|
|
67
|
+
/** Whether a URL's host, as written, is a local name or a non-public IP address. */
|
|
68
|
+
function isPrivateHostName(hostname) {
|
|
69
|
+
const bare = hostname.replace(/^\[|\]$/g, '');
|
|
70
|
+
return (0, net_1.isIP)(bare) ? isPrivateIp(bare) : LOCAL_NAME.test(bare);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a host, resolved now, reaches a non-public address: a name can resolve to one even
|
|
74
|
+
* if it doesn't look like it, and resolve differently later, so it's checked right before each
|
|
75
|
+
* request.
|
|
76
|
+
*/
|
|
77
|
+
async function resolvesToPrivateAddress(hostname) {
|
|
78
|
+
const bare = hostname.replace(/^\[|\]$/g, '');
|
|
79
|
+
if (isPrivateHostName(bare))
|
|
80
|
+
return true;
|
|
81
|
+
if ((0, net_1.isIP)(bare))
|
|
82
|
+
return false;
|
|
83
|
+
const addresses = await dns_1.promises.lookup(bare, { all: true });
|
|
84
|
+
return !addresses.length || addresses.some(({ address }) => isPrivateIp(address));
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.apiConnectionRouter = apiConnectionRouter;
|
|
4
|
-
const express_1 = require("express");
|
|
5
|
-
const manager_1 = require("../../models/manager");
|
|
6
|
-
const auth_1 = require("../../utils/auth");
|
|
7
|
-
const apiConnection_1 = require("../../services/apiConnection");
|
|
8
|
-
const VALID_METHODS = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'];
|
|
9
|
-
function apiConnectionRouter(config) {
|
|
10
|
-
const router = (0, express_1.Router)();
|
|
11
|
-
const Project = (0, manager_1.getModel)('projects');
|
|
12
|
-
// List API connections for a project
|
|
13
|
-
router.get('/projects/:id/api/connections', async (req, res) => {
|
|
14
|
-
try {
|
|
15
|
-
const auth = req.auth;
|
|
16
|
-
const projectId = req.params.id;
|
|
17
|
-
const project = await Project.findOne({ _id: projectId });
|
|
18
|
-
if (!project) {
|
|
19
|
-
return res.status(404).json({ message: 'Project not found' });
|
|
20
|
-
}
|
|
21
|
-
if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
22
|
-
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
23
|
-
}
|
|
24
|
-
return res.status(200).json({ data: project.apiConnections || [] });
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
res.status(500).json({ message: 'Failed to fetch API connections', details: err.message });
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
// Add a new API connection to a project
|
|
31
|
-
router.post('/projects/:id/api/connections', async (req, res) => {
|
|
32
|
-
try {
|
|
33
|
-
const auth = req.auth;
|
|
34
|
-
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
35
|
-
return res.status(403).json({ message: 'Permission denied' });
|
|
36
|
-
}
|
|
37
|
-
const projectId = req.params.id;
|
|
38
|
-
const { name, baseUrl, method, description, credentialId, prefix } = req.body;
|
|
39
|
-
if (!name || !baseUrl) {
|
|
40
|
-
return res.status(400).json({ message: 'name and baseUrl are required' });
|
|
41
|
-
}
|
|
42
|
-
if (!VALID_METHODS.includes(method)) {
|
|
43
|
-
return res.status(400).json({ message: `method must be one of: ${VALID_METHODS.join(', ')}` });
|
|
44
|
-
}
|
|
45
|
-
const project = await Project.findOne({ _id: projectId });
|
|
46
|
-
if (!project) {
|
|
47
|
-
return res.status(404).json({ message: 'Project not found' });
|
|
48
|
-
}
|
|
49
|
-
if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
50
|
-
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
51
|
-
}
|
|
52
|
-
if (credentialId) {
|
|
53
|
-
const credExists = project.credentials?.some((c) => c.id === credentialId);
|
|
54
|
-
if (!credExists) {
|
|
55
|
-
return res.status(400).json({ message: 'Credential not found in project' });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const connection = await (0, apiConnection_1.addApiConnection)(config, projectId, {
|
|
59
|
-
name,
|
|
60
|
-
baseUrl,
|
|
61
|
-
method,
|
|
62
|
-
prefix,
|
|
63
|
-
...(description !== undefined && { description }),
|
|
64
|
-
...(credentialId !== undefined && { credentialId }),
|
|
65
|
-
});
|
|
66
|
-
return res.status(201).json({ data: connection });
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
res.status(500).json({ message: 'Failed to add API connection', details: err.message });
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
// Delete an API connection from a project
|
|
73
|
-
router.delete('/projects/:id/api/connections/:connectionId', async (req, res) => {
|
|
74
|
-
try {
|
|
75
|
-
const auth = req.auth;
|
|
76
|
-
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
77
|
-
return res.status(403).json({ message: 'Permission denied' });
|
|
78
|
-
}
|
|
79
|
-
const projectId = req.params.id;
|
|
80
|
-
const { connectionId } = req.params;
|
|
81
|
-
const project = await Project.findOne({ _id: projectId });
|
|
82
|
-
if (!project) {
|
|
83
|
-
return res.status(404).json({ message: 'Project not found' });
|
|
84
|
-
}
|
|
85
|
-
if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
86
|
-
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
87
|
-
}
|
|
88
|
-
const exists = project.apiConnections?.some((c) => c.id === connectionId);
|
|
89
|
-
if (!exists) {
|
|
90
|
-
return res.status(404).json({ message: 'API connection not found' });
|
|
91
|
-
}
|
|
92
|
-
await (0, apiConnection_1.deleteApiConnection)(config, projectId, connectionId);
|
|
93
|
-
return res.status(200).json({ message: 'API connection deleted successfully.' });
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
res.status(500).json({ message: 'Failed to delete API connection', details: err.message });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
return router;
|
|
100
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.credentialRouter = credentialRouter;
|
|
4
|
-
const express_1 = require("express");
|
|
5
|
-
const manager_1 = require("../../models/manager");
|
|
6
|
-
const auth_1 = require("../../utils/auth");
|
|
7
|
-
const sanitize_1 = require("../../utils/sanitize");
|
|
8
|
-
const credential_1 = require("../../services/credential");
|
|
9
|
-
function credentialRouter(config) {
|
|
10
|
-
const router = (0, express_1.Router)();
|
|
11
|
-
const Project = (0, manager_1.getModel)('projects');
|
|
12
|
-
// Add a new credential to a project
|
|
13
|
-
router.post('/projects/:id/credentials', async (req, res) => {
|
|
14
|
-
try {
|
|
15
|
-
const auth = req.auth;
|
|
16
|
-
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
17
|
-
return res.status(403).json({ message: 'Permission denied' });
|
|
18
|
-
}
|
|
19
|
-
const projectId = req.params.id;
|
|
20
|
-
const { id, key, value } = req.body;
|
|
21
|
-
if (!key || !value) {
|
|
22
|
-
return res.status(400).json({ message: 'Missing credential key or value' });
|
|
23
|
-
}
|
|
24
|
-
const project = await Project.findOne({ _id: projectId });
|
|
25
|
-
if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
26
|
-
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
27
|
-
}
|
|
28
|
-
const updatedProject = await (0, credential_1.addCredential)(config, projectId, { id, key, value });
|
|
29
|
-
res.status(201).json({ message: 'Credential added successfully', project: (0, sanitize_1.sanitizeProject)(updatedProject) });
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
res.status(500).json({ message: 'Failed to add credential', details: err.message });
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
// Update an existing credential in a project
|
|
36
|
-
router.patch('/projects/:id/credentials/:credentialId', async (req, res) => {
|
|
37
|
-
try {
|
|
38
|
-
const auth = req.auth;
|
|
39
|
-
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
40
|
-
return res.status(403).json({ message: 'Permission denied' });
|
|
41
|
-
}
|
|
42
|
-
const projectId = req.params.id;
|
|
43
|
-
const credentialId = req.params.credentialId;
|
|
44
|
-
const { key, value } = req.body;
|
|
45
|
-
if (!key && !value) {
|
|
46
|
-
return res.status(400).json({ message: 'No update fields provided' });
|
|
47
|
-
}
|
|
48
|
-
const project = await Project.findOne({ _id: projectId });
|
|
49
|
-
if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
50
|
-
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
51
|
-
}
|
|
52
|
-
const updatedProject = await (0, credential_1.updateCredential)(config, projectId, credentialId, { key, value });
|
|
53
|
-
res.status(200).json({ message: 'Credential updated successfully', project: (0, sanitize_1.sanitizeProject)(updatedProject) });
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
res.status(500).json({ message: 'Failed to update credential', details: err.message });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
// Delete a credential from a project
|
|
60
|
-
router.delete('/projects/:id/credentials/:credentialId', async (req, res) => {
|
|
61
|
-
try {
|
|
62
|
-
const auth = req.auth;
|
|
63
|
-
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
64
|
-
return res.status(403).json({ message: 'Permission denied' });
|
|
65
|
-
}
|
|
66
|
-
const projectId = req.params.id;
|
|
67
|
-
const credentialId = req.params.credentialId;
|
|
68
|
-
const project = await Project.findOne({ _id: projectId });
|
|
69
|
-
if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
70
|
-
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
71
|
-
}
|
|
72
|
-
const updatedProject = await (0, credential_1.deleteCredential)(config, projectId, credentialId);
|
|
73
|
-
res.status(200).json({ message: 'Credential deleted successfully', project: (0, sanitize_1.sanitizeProject)(updatedProject) });
|
|
74
|
-
}
|
|
75
|
-
catch (err) {
|
|
76
|
-
res.status(500).json({ message: 'Failed to delete credential', details: err.message });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return router;
|
|
80
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { StreamByConfig, ApiConnection, ApiConnectionMethod } from '../types';
|
|
2
|
-
export declare function addApiConnection(config: StreamByConfig, projectId: string, data: {
|
|
3
|
-
name: string;
|
|
4
|
-
apiUrl: string;
|
|
5
|
-
method: ApiConnectionMethod;
|
|
6
|
-
prefix?: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
credentialId?: string;
|
|
9
|
-
}): Promise<ApiConnection>;
|
|
10
|
-
export declare function updateApiConnection(config: StreamByConfig, projectId: string, connectionId: string, data: {
|
|
11
|
-
name?: string;
|
|
12
|
-
apiUrl?: string;
|
|
13
|
-
method?: ApiConnectionMethod;
|
|
14
|
-
prefix?: string;
|
|
15
|
-
description?: string;
|
|
16
|
-
credentialId?: string;
|
|
17
|
-
}): Promise<ApiConnection>;
|
|
18
|
-
export declare function deleteApiConnection(config: StreamByConfig, projectId: string, connectionId: string): Promise<void>;
|