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
@@ -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;
@@ -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/storage provider. Delegates to config.canUseBuiltin
8
- // when the mounting app implements it (e.g. Nhexa-API checking user_subscriptions);
9
- // defaults to allow when absent, preserving pre-BYOC behavior for deploys without gating.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "streamby-core",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "StreamBy middleware framework for media storage management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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,3 +0,0 @@
1
- import { Router } from 'express';
2
- import { StreamByConfig } from '../../types';
3
- export declare function credentialRouter(config: StreamByConfig): Router;
@@ -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>;