streamby-core 0.28.1 → 0.28.4

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/README.md CHANGED
@@ -1,283 +1,283 @@
1
- # @streamby/core
2
-
3
- Middleware framework for building storage-agnostic media APIs.
4
- Part of the [StreamBy](https://streamby.nhexa.cl) ecosystem by TerminalCore Labs.
5
-
6
- ---
7
-
8
- ## 🚀 What is it?
9
-
10
- `@streamby/core` is a plug-and-play middleware for Express (or compatible frameworks) that enables file uploads, listings and **multi-project access** over services like:
11
-
12
- - ✅ AWS S3
13
- - ✅ Google Cloud Storage (soon)
14
- - ✅ Cloudflare R2 (soon)
15
- - ✅ Local/Personal servers (soon)
16
-
17
- It's designed to be installed inside your existing API as a library — **no need to run a separate backend**.
18
-
19
- ---
20
-
21
- ## 📦 Installation
22
-
23
- ```bash
24
- npm install @streamby/core ws
25
- npm install -D @types/ws
26
- ```
27
-
28
- ---
29
-
30
- ## 🧱 Basic usage
31
-
32
- ```ts
33
- import http from 'http';
34
- import express from 'express';
35
- import { WebSocketServer } from 'ws';
36
- import { createStreamByRouter } from '@streamby/core';
37
-
38
- const app = express();
39
- const server = http.createServer(app);
40
- const wss = new WebSocketServer({ server, path: '/streamby/ws' });
41
-
42
- app.use('/streamby', express.json(), createStreamByRouter({
43
- authProvider: async (req) => {
44
- // validate token/session and return user info
45
- return { userId: 'user-id', username: 'username', role: 'admin' };
46
- },
47
- databases: [
48
- { id: 'mongo', type: 'nosql', connectionString: process.env.MONGO_URI! },
49
- { id: 'postgres', type: 'sql', connectionString: process.env.POSTGRES_URI!, main: true },
50
- ],
51
- storageProviders: [
52
- {
53
- type: 's3',
54
- config: {
55
- bucket: process.env.AWS_BUCKET!,
56
- region: process.env.AWS_REGION!,
57
- accessKeyId: process.env.AWS_ACCESS_KEY!,
58
- secretAccessKey: process.env.AWS_SECRET_KEY!,
59
- },
60
- },
61
- ],
62
- encrypt: process.env.STREAMBY_ENCRYPTION_KEY,
63
- websocket: { server: wss },
64
- }));
65
-
66
- server.listen(3000);
67
- ```
68
-
69
- > **Important:** use `http.createServer(app)` and pass `server.listen()` instead of `app.listen()`.
70
- > This is required so the `WebSocketServer` shares the same port as the HTTP server.
71
-
72
- ---
73
-
74
- ## 🗄️ Database Schema Setup
75
-
76
- `@streamby/core` requires a pre-configured database schema for its internal operations. It does not manage database connections directly; instead, it expects the host application to provide initialized connection instances. This ensures full control and flexibility for the host environment.
77
-
78
- The library will only interact with the specified schema and will not touch other schemas in your database. The `reset` option is opt-in and will only drop and recreate the specified `streamby` schema if explicitly set to `true`.
79
-
80
- ### PostgreSQL Example
81
-
82
- ```ts
83
- import { Pool } from "pg";
84
- import { setupStreambyPg } from "@streamby/core/pg/setup"; // Adjust path as per your package structure
85
-
86
- const pool = new Pool({ connectionString: process.env.DATABASE_URL });
87
-
88
- // Call setupStreambyPg to ensure the schema and tables are created
89
- // reset: false (default) ensures idempotency and no data loss
90
- await setupStreambyPg({ pool, schema: "streamby", reset: false });
91
- ```
92
-
93
- ### MongoDB Example
94
-
95
- ```ts
96
- import { MongoClient } from "mongodb";
97
- import { setupStreambyMongo } from "@streamby/core/mongo/setup"; // Adjust path as per your package structure
98
-
99
- const client = new MongoClient(process.env.MONGO_URL!);
100
-
101
- // Connect the MongoDB client
102
- await client.connect();
103
-
104
- // Call setupStreambyMongo to ensure collections and indexes are created
105
- await setupStreambyMongo({ client, dbName: "streamby" });
106
- ```
107
-
108
- ```ts
109
- import express from 'express';
110
- import { createStreamByRouter } from '@streamby/core';
111
-
112
- const app = express();
113
-
114
- app.use('/streamby', createStreamByRouter({
115
- storageProvider: {
116
- type: 's3',
117
- config: {
118
- bucket: 'your-bucket-name',
119
- region: 'your-region',
120
- accessKeyId: 'YOUR_AWS_KEY',
121
- secretAccessKey: 'YOUR_AWS_SECRET'
122
- }
123
- },
124
- authProvider: async (req) => {
125
- // extract token/cookie and validate
126
- return {
127
- userId: 'demo',
128
- projects: ['demo-project'],
129
- role: 'admin',
130
- };
131
- },
132
- projectProvider: async (projectId) => {
133
- // fetch project from your DB
134
- return {
135
- id: projectId,
136
- name: 'Demo Project',
137
- description: 'Demo for StreamBy integration',
138
- rootFolders: [],
139
- settings: { allowUpload: true }
140
- };
141
- }
142
- }));
143
-
144
- app.listen(3000);
145
- ```
146
-
147
- ---
148
-
149
- ## 🧪 Testing
150
-
151
- ```bash
152
- npm run test
153
- ```
154
-
155
- Unit tests use [Vitest](https://vitest.dev) and [Supertest](https://www.npmjs.com/package/supertest).
156
-
157
- ---
158
-
159
- ## 📁 Features
160
-
161
- - 📂 Multi-project access control
162
- - 🔐 Project-aware file uploads & listings
163
- - 🧹 Modular adapters per storage provider
164
- - 🧰 Built-in testability with mock auth & storage
165
- - ✨ Simple to extend for custom business logic
166
-
167
- ---
168
-
169
- ## 🗺️ API Endpoints
170
-
171
- Here's a summary of the key API endpoints provided by `@streamby/core`:
172
-
173
- ### `GET /streamby/projects`
174
-
175
- Lists all projects accessible by the authenticated user.
176
-
177
- **Query Parameters:**
178
- - `archived`: (Optional) Filter projects by their archived status.
179
- - `true`: Returns only archived projects.
180
- - `false`: Returns only unarchived projects.
181
- - (Omitted): Returns all projects (both archived and unarchived).
182
-
183
- **Example Response:**
184
- ```json
185
- {
186
- "projects": [
187
- {
188
- "id": "project1_id",
189
- "dbType": "nosql",
190
- "name": "My NoSQL Project",
191
- "image": "url_to_image",
192
- "archived": false
193
- },
194
- {
195
- "id": "project2_id",
196
- "dbType": "sql",
197
- "name": "My SQL Project",
198
- "image": "url_to_image",
199
- "archived": true
200
- }
201
- ]
202
- }
203
- ```
204
-
205
- ### `PATCH /streamby/projects/:id/archive`
206
-
207
- Archives a specific project. After archiving, it returns the complete list of projects for the user, including the updated project.
208
-
209
- **Example Response:**
210
- ```json
211
- {
212
- "success": true,
213
- "projects": [
214
- {
215
- "id": "project1_id",
216
- "dbType": "nosql",
217
- "name": "My NoSQL Project",
218
- "image": "url_to_image",
219
- "archived": true
220
- },
221
- {
222
- "id": "project2_id",
223
- "dbType": "sql",
224
- "name": "My SQL Project",
225
- "image": "url_to_image",
226
- "archived": false
227
- }
228
- ]
229
- }
230
- ```
231
-
232
- ### `PATCH /streamby/projects/:id/unarchive`
233
-
234
- Unarchives a specific project. After unarchiving, it returns the complete list of projects for the user, including the updated project.
235
-
236
- **Example Response:**
237
- ```json
238
- {
239
- "success": true,
240
- "projects": [
241
- {
242
- "id": "project1_id",
243
- "dbType": "nosql",
244
- "name": "My NoSQL Project",
245
- "image": "url_to_image",
246
- "archived": false
247
- },
248
- {
249
- "id": "project2_id",
250
- "dbType": "sql",
251
- "name": "My SQL Project",
252
- "image": "url_to_image",
253
- "archived": false
254
- }
255
- ]
256
- }
257
- ```
258
-
259
- ---
260
-
261
- ## 🚣 Roadmap
262
-
263
- - [ ] Support for Google Cloud Storage
264
- - [ ] CLI tool for uploading files
265
- - [ ] Role-based access control middleware
266
- - [ ] Plugin system for extended routes
267
- - [ ] Streaming support (HLS, audio, etc.)
268
-
269
- ---
270
-
271
- ## 🧑‍💻 Maintained by
272
-
273
- **TerminalCore Labs** – part of [Nhexa Entertainment](https://nhexa.cl)
274
- Developed as open-source infrastructure for creative and media-oriented apps.
275
-
276
- Visit us [here](https://terminalcore.cl)
277
-
278
- ---
279
-
280
- ## 📟 License
281
-
282
- [MIT](./LICENSE)
283
-
1
+ # @streamby/core
2
+
3
+ Middleware framework for building storage-agnostic media APIs.
4
+ Part of the [StreamBy](https://streamby.nhexa.cl) ecosystem by TerminalCore Labs.
5
+
6
+ ---
7
+
8
+ ## 🚀 What is it?
9
+
10
+ `@streamby/core` is a plug-and-play middleware for Express (or compatible frameworks) that enables file uploads, listings and **multi-project access** over services like:
11
+
12
+ - ✅ AWS S3
13
+ - ✅ Google Cloud Storage (soon)
14
+ - ✅ Cloudflare R2 (soon)
15
+ - ✅ Local/Personal servers (soon)
16
+
17
+ It's designed to be installed inside your existing API as a library — **no need to run a separate backend**.
18
+
19
+ ---
20
+
21
+ ## 📦 Installation
22
+
23
+ ```bash
24
+ npm install @streamby/core ws
25
+ npm install -D @types/ws
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 🧱 Basic usage
31
+
32
+ ```ts
33
+ import http from 'http';
34
+ import express from 'express';
35
+ import { WebSocketServer } from 'ws';
36
+ import { createStreamByRouter } from '@streamby/core';
37
+
38
+ const app = express();
39
+ const server = http.createServer(app);
40
+ const wss = new WebSocketServer({ server, path: '/streamby/ws' });
41
+
42
+ app.use('/streamby', express.json(), createStreamByRouter({
43
+ authProvider: async (req) => {
44
+ // validate token/session and return user info
45
+ return { userId: 'user-id', username: 'username', role: 'admin' };
46
+ },
47
+ databases: [
48
+ { id: 'mongo', type: 'nosql', connectionString: process.env.MONGO_URI! },
49
+ { id: 'postgres', type: 'sql', connectionString: process.env.POSTGRES_URI!, main: true },
50
+ ],
51
+ storageProviders: [
52
+ {
53
+ type: 's3',
54
+ config: {
55
+ bucket: process.env.AWS_BUCKET!,
56
+ region: process.env.AWS_REGION!,
57
+ accessKeyId: process.env.AWS_ACCESS_KEY!,
58
+ secretAccessKey: process.env.AWS_SECRET_KEY!,
59
+ },
60
+ },
61
+ ],
62
+ encrypt: process.env.STREAMBY_ENCRYPTION_KEY,
63
+ websocket: { server: wss },
64
+ }));
65
+
66
+ server.listen(3000);
67
+ ```
68
+
69
+ > **Important:** use `http.createServer(app)` and pass `server.listen()` instead of `app.listen()`.
70
+ > This is required so the `WebSocketServer` shares the same port as the HTTP server.
71
+
72
+ ---
73
+
74
+ ## 🗄️ Database Schema Setup
75
+
76
+ `@streamby/core` requires a pre-configured database schema for its internal operations. It does not manage database connections directly; instead, it expects the host application to provide initialized connection instances. This ensures full control and flexibility for the host environment.
77
+
78
+ The library will only interact with the specified schema and will not touch other schemas in your database. The `reset` option is opt-in and will only drop and recreate the specified `streamby` schema if explicitly set to `true`.
79
+
80
+ ### PostgreSQL Example
81
+
82
+ ```ts
83
+ import { Pool } from "pg";
84
+ import { setupStreambyPg } from "@streamby/core/pg/setup"; // Adjust path as per your package structure
85
+
86
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
87
+
88
+ // Call setupStreambyPg to ensure the schema and tables are created
89
+ // reset: false (default) ensures idempotency and no data loss
90
+ await setupStreambyPg({ pool, schema: "streamby", reset: false });
91
+ ```
92
+
93
+ ### MongoDB Example
94
+
95
+ ```ts
96
+ import { MongoClient } from "mongodb";
97
+ import { setupStreambyMongo } from "@streamby/core/mongo/setup"; // Adjust path as per your package structure
98
+
99
+ const client = new MongoClient(process.env.MONGO_URL!);
100
+
101
+ // Connect the MongoDB client
102
+ await client.connect();
103
+
104
+ // Call setupStreambyMongo to ensure collections and indexes are created
105
+ await setupStreambyMongo({ client, dbName: "streamby" });
106
+ ```
107
+
108
+ ```ts
109
+ import express from 'express';
110
+ import { createStreamByRouter } from '@streamby/core';
111
+
112
+ const app = express();
113
+
114
+ app.use('/streamby', createStreamByRouter({
115
+ storageProvider: {
116
+ type: 's3',
117
+ config: {
118
+ bucket: 'your-bucket-name',
119
+ region: 'your-region',
120
+ accessKeyId: 'YOUR_AWS_KEY',
121
+ secretAccessKey: 'YOUR_AWS_SECRET'
122
+ }
123
+ },
124
+ authProvider: async (req) => {
125
+ // extract token/cookie and validate
126
+ return {
127
+ userId: 'demo',
128
+ projects: ['demo-project'],
129
+ role: 'admin',
130
+ };
131
+ },
132
+ projectProvider: async (projectId) => {
133
+ // fetch project from your DB
134
+ return {
135
+ id: projectId,
136
+ name: 'Demo Project',
137
+ description: 'Demo for StreamBy integration',
138
+ rootFolders: [],
139
+ settings: { allowUpload: true }
140
+ };
141
+ }
142
+ }));
143
+
144
+ app.listen(3000);
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 🧪 Testing
150
+
151
+ ```bash
152
+ npm run test
153
+ ```
154
+
155
+ Unit tests use [Vitest](https://vitest.dev) and [Supertest](https://www.npmjs.com/package/supertest).
156
+
157
+ ---
158
+
159
+ ## 📁 Features
160
+
161
+ - 📂 Multi-project access control
162
+ - 🔐 Project-aware file uploads & listings
163
+ - 🧹 Modular adapters per storage provider
164
+ - 🧰 Built-in testability with mock auth & storage
165
+ - ✨ Simple to extend for custom business logic
166
+
167
+ ---
168
+
169
+ ## 🗺️ API Endpoints
170
+
171
+ Here's a summary of the key API endpoints provided by `@streamby/core`:
172
+
173
+ ### `GET /streamby/projects`
174
+
175
+ Lists all projects accessible by the authenticated user.
176
+
177
+ **Query Parameters:**
178
+ - `archived`: (Optional) Filter projects by their archived status.
179
+ - `true`: Returns only archived projects.
180
+ - `false`: Returns only unarchived projects.
181
+ - (Omitted): Returns all projects (both archived and unarchived).
182
+
183
+ **Example Response:**
184
+ ```json
185
+ {
186
+ "projects": [
187
+ {
188
+ "id": "project1_id",
189
+ "dbType": "nosql",
190
+ "name": "My NoSQL Project",
191
+ "image": "url_to_image",
192
+ "archived": false
193
+ },
194
+ {
195
+ "id": "project2_id",
196
+ "dbType": "sql",
197
+ "name": "My SQL Project",
198
+ "image": "url_to_image",
199
+ "archived": true
200
+ }
201
+ ]
202
+ }
203
+ ```
204
+
205
+ ### `PATCH /streamby/projects/:id/archive`
206
+
207
+ Archives a specific project. After archiving, it returns the complete list of projects for the user, including the updated project.
208
+
209
+ **Example Response:**
210
+ ```json
211
+ {
212
+ "success": true,
213
+ "projects": [
214
+ {
215
+ "id": "project1_id",
216
+ "dbType": "nosql",
217
+ "name": "My NoSQL Project",
218
+ "image": "url_to_image",
219
+ "archived": true
220
+ },
221
+ {
222
+ "id": "project2_id",
223
+ "dbType": "sql",
224
+ "name": "My SQL Project",
225
+ "image": "url_to_image",
226
+ "archived": false
227
+ }
228
+ ]
229
+ }
230
+ ```
231
+
232
+ ### `PATCH /streamby/projects/:id/unarchive`
233
+
234
+ Unarchives a specific project. After unarchiving, it returns the complete list of projects for the user, including the updated project.
235
+
236
+ **Example Response:**
237
+ ```json
238
+ {
239
+ "success": true,
240
+ "projects": [
241
+ {
242
+ "id": "project1_id",
243
+ "dbType": "nosql",
244
+ "name": "My NoSQL Project",
245
+ "image": "url_to_image",
246
+ "archived": false
247
+ },
248
+ {
249
+ "id": "project2_id",
250
+ "dbType": "sql",
251
+ "name": "My SQL Project",
252
+ "image": "url_to_image",
253
+ "archived": false
254
+ }
255
+ ]
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 🚣 Roadmap
262
+
263
+ - [ ] Support for Google Cloud Storage
264
+ - [ ] CLI tool for uploading files
265
+ - [ ] Role-based access control middleware
266
+ - [ ] Plugin system for extended routes
267
+ - [ ] Streaming support (HLS, audio, etc.)
268
+
269
+ ---
270
+
271
+ ## 🧑‍💻 Maintained by
272
+
273
+ **TerminalCore Labs** – part of [Nhexa Entertainment](https://nhexa.cl)
274
+ Developed as open-source infrastructure for creative and media-oriented apps.
275
+
276
+ Visit us [here](https://terminalcore.cl)
277
+
278
+ ---
279
+
280
+ ## 📟 License
281
+
282
+ [MIT](./LICENSE)
283
+
@@ -43,83 +43,83 @@ const getConnection = (id) => {
43
43
  exports.getConnection = getConnection;
44
44
  const ensureTablesExist = async (pool) => {
45
45
  try {
46
- await pool.query(`
47
- CREATE SCHEMA IF NOT EXISTS streamby;
48
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
46
+ await pool.query(`
47
+ CREATE SCHEMA IF NOT EXISTS streamby;
48
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
49
49
  `);
50
50
  // 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 $$;
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 $$;
56
56
  `);
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
- credentials JSONB DEFAULT '[]',
66
- "apiConnections" JSONB DEFAULT '[]',
67
- "dbConnections" JSONB DEFAULT '[]',
68
- "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
69
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
70
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
71
- );
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
+ credentials JSONB DEFAULT '[]',
66
+ "apiConnections" JSONB DEFAULT '[]',
67
+ "dbConnections" JSONB DEFAULT '[]',
68
+ "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
69
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
70
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
71
+ );
72
72
  `);
73
73
  console.log('✅ "projects" table ensured to exist.');
74
74
  // Add JSONB columns to existing tables that predate this schema version
75
- await pool.query(`
76
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
77
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
78
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials JSONB DEFAULT '[]';
79
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections" JSONB DEFAULT '[]';
80
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
75
+ await pool.query(`
76
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
77
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
78
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials JSONB DEFAULT '[]';
79
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections" JSONB DEFAULT '[]';
80
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
81
81
  `);
82
82
  // Ensure dbType exists with a non-null default
83
- await pool.query(`
84
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
83
+ await pool.query(`
84
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
85
85
  `);
86
- await pool.query(`
87
- CREATE TABLE IF NOT EXISTS streamby.project_members (
88
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
89
- "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
90
- "userId" VARCHAR(255) NOT NULL,
91
- role VARCHAR(255) NOT NULL DEFAULT 'member',
92
- status VARCHAR(50) NOT NULL DEFAULT 'active',
93
- archived BOOLEAN NOT NULL DEFAULT FALSE,
94
- "archivedBy" VARCHAR(255),
95
- "archivedAt" TIMESTAMP WITH TIME ZONE,
96
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
97
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
98
- UNIQUE ("projectId", "userId")
99
- );
86
+ await pool.query(`
87
+ CREATE TABLE IF NOT EXISTS streamby.project_members (
88
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
89
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
90
+ "userId" VARCHAR(255) NOT NULL,
91
+ role VARCHAR(255) NOT NULL DEFAULT 'member',
92
+ status VARCHAR(50) NOT NULL DEFAULT 'active',
93
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
94
+ "archivedBy" VARCHAR(255),
95
+ "archivedAt" TIMESTAMP WITH TIME ZONE,
96
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
97
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
98
+ UNIQUE ("projectId", "userId")
99
+ );
100
100
  `);
101
- await pool.query(`
102
- ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
101
+ await pool.query(`
102
+ ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
103
103
  `);
104
104
  console.log('✅ "project_members" table ensured to exist.');
105
105
  // Migrate projectId FK column from UUID to TEXT if table predates this schema version
106
- await pool.query(`
107
- DO $$ BEGIN
108
- ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
109
- ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
110
- ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
111
- FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
112
- EXCEPTION WHEN OTHERS THEN NULL; END $$;
106
+ await pool.query(`
107
+ DO $$ BEGIN
108
+ ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
109
+ ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
110
+ ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
111
+ FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
112
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
113
113
  `);
114
- await pool.query(`
115
- CREATE TABLE IF NOT EXISTS streamby.exports (
116
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
117
- "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
118
- status VARCHAR(50) NOT NULL DEFAULT 'pending',
119
- "filePath" TEXT,
120
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
121
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
122
- );
114
+ await pool.query(`
115
+ CREATE TABLE IF NOT EXISTS streamby.exports (
116
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
117
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
118
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
119
+ "filePath" TEXT,
120
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
121
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
122
+ );
123
123
  `);
124
124
  console.log('✅ "exports" table ensured to exist.');
125
125
  }
@@ -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
  };
@@ -295,51 +295,45 @@ function exportRouter(config) {
295
295
  if (!exportMetadata) {
296
296
  return res.status(404).json({ message: 'Export not found in this project' });
297
297
  }
298
- // Authenticated project members bypass origin checks (e.g. dev environments).
299
- const auth = await config.authProvider(req).catch(() => null);
300
- const isMember = auth?.userId && (0, auth_1.isProjectMember)(project, auth.userId);
301
- if (!isMember) {
302
- const origin = req.headers.origin;
303
- let effectiveAllowedOrigins = exportMetadata.allowedOrigin;
304
- const projectOrigins = project.allowedOrigin;
305
- // If export's allowedOrigin is empty, ['*'], or not set, it inherits from the project.
306
- if (!effectiveAllowedOrigins || effectiveAllowedOrigins.length === 0 || (effectiveAllowedOrigins.length === 1 && effectiveAllowedOrigins[0] === '*')) {
307
- effectiveAllowedOrigins = projectOrigins;
308
- }
309
- else {
310
- // If the export has its own list, ensure it's a subset of the project's list (if project is not public).
311
- if (projectOrigins && !projectOrigins.includes('*')) {
312
- const isSubset = effectiveAllowedOrigins.every((o) => projectOrigins.includes(o));
313
- if (!isSubset) {
314
- return res.status(403).json({ message: 'Unauthorized: Export origins are not allowed by the parent project.' });
315
- }
298
+ const origin = req.headers.origin;
299
+ let effectiveAllowedOrigins = exportMetadata.allowedOrigin;
300
+ const projectOrigins = project.allowedOrigin;
301
+ // If export's allowedOrigin is empty, ['*'], or not set, it inherits from the project.
302
+ if (!effectiveAllowedOrigins || effectiveAllowedOrigins.length === 0 || (effectiveAllowedOrigins.length === 1 && effectiveAllowedOrigins[0] === '*')) {
303
+ effectiveAllowedOrigins = projectOrigins;
304
+ }
305
+ else {
306
+ if (projectOrigins && !projectOrigins.includes('*')) {
307
+ const isSubset = effectiveAllowedOrigins.every((o) => projectOrigins.includes(o));
308
+ if (!isSubset) {
309
+ return res.status(403).json({ message: 'Unauthorized: Export origins are not allowed by the parent project.' });
316
310
  }
317
311
  }
318
- // Dev mode: auto-allow localhost requests on configured ports.
319
- let devModeAllowed = false;
320
- if (exportMetadata.devMode && origin) {
321
- const match = origin.match(/^https?:\/\/localhost:(\d+)$/);
322
- if (match) {
323
- const port = parseInt(match[1]);
324
- const allowedPorts = exportMetadata.devPorts?.length
325
- ? exportMetadata.devPorts
326
- : [3000, 5173, 8080, 4200];
327
- if (allowedPorts.includes(port)) {
328
- devModeAllowed = true;
329
- res.header('Access-Control-Allow-Origin', origin);
330
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
331
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
332
- }
312
+ }
313
+ // Dev mode: auto-allow localhost requests on configured ports.
314
+ let devModeAllowed = false;
315
+ if (exportMetadata.devMode && origin) {
316
+ const match = origin.match(/^https?:\/\/localhost:(\d+)$/);
317
+ if (match) {
318
+ const port = parseInt(match[1]);
319
+ const allowedPorts = exportMetadata.devPorts?.length
320
+ ? exportMetadata.devPorts
321
+ : [3000, 5173, 8080, 4200];
322
+ if (allowedPorts.includes(port)) {
323
+ devModeAllowed = true;
324
+ res.header('Access-Control-Allow-Origin', origin);
325
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
326
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
333
327
  }
334
328
  }
335
- if (!devModeAllowed) {
336
- if (!origin && !(effectiveAllowedOrigins && effectiveAllowedOrigins.includes('*'))) {
337
- return res.status(403).json({ message: 'Origin header required' });
338
- }
339
- const isAllowed = effectiveAllowedOrigins && (effectiveAllowedOrigins.includes('*') || (origin && effectiveAllowedOrigins.includes(origin)));
340
- if (!isAllowed) {
341
- return res.status(403).json({ message: 'Unauthorized' });
342
- }
329
+ }
330
+ if (!devModeAllowed) {
331
+ if (!origin && !(effectiveAllowedOrigins && effectiveAllowedOrigins.includes('*'))) {
332
+ return res.status(403).json({ message: 'Origin header required' });
333
+ }
334
+ const isAllowed = effectiveAllowedOrigins && (effectiveAllowedOrigins.includes('*') || (origin && effectiveAllowedOrigins.includes(origin)));
335
+ if (!isAllowed) {
336
+ return res.status(403).json({ message: 'Unauthorized' });
343
337
  }
344
338
  }
345
339
  let data;
@@ -148,6 +148,39 @@ function storageRouter(config) {
148
148
  res.status(500).json({ message: 'Failed to generate upload URL', details: err.message });
149
149
  }
150
150
  });
151
+ router.get('/projects/:projectId/storage/files/:fileId/replace-url', async (req, res) => {
152
+ try {
153
+ const auth = req.auth;
154
+ const { projectId, fileId } = req.params;
155
+ const { contentType, fileName } = req.query;
156
+ if (!contentType || !fileName) {
157
+ return res.status(400).json({ message: 'Query params contentType and fileName are required' });
158
+ }
159
+ const project = await Project.findOne({ _id: projectId });
160
+ if (!project)
161
+ return res.status(404).json({ message: 'Project not found' });
162
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
163
+ return res.status(403).json({ message: 'Unauthorized access' });
164
+ const collection = getRawFilesCollection();
165
+ if (!collection)
166
+ return res.status(500).json({ message: 'Database not available' });
167
+ const fileDoc = await collection.findOne({ fileId, projectId });
168
+ if (!fileDoc)
169
+ return res.status(404).json({ message: 'File not found' });
170
+ if (!validateContentType(contentType, fileName, fileDoc.category)) {
171
+ return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
172
+ }
173
+ if (!adapter.getPresignedUploadUrl) {
174
+ return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
175
+ }
176
+ const url = await adapter.getPresignedUploadUrl(fileDoc.storageKey, contentType);
177
+ await collection.updateOne({ fileId, projectId }, { $set: { contentType, updatedAt: new Date() } });
178
+ res.json({ url, storageKey: fileDoc.storageKey, fileId });
179
+ }
180
+ catch (err) {
181
+ res.status(500).json({ message: 'Failed to generate replace URL', details: err.message });
182
+ }
183
+ });
151
184
  router.patch('/projects/:projectId/storage/files/:fileId', async (req, res) => {
152
185
  try {
153
186
  const auth = req.auth;
@@ -339,6 +372,42 @@ function storageRouter(config) {
339
372
  res.status(500).json({ message: 'Failed to list files', details: err.message });
340
373
  }
341
374
  });
375
+ router.get('/projects/:projectId/connections/storage/:connId/files/:fileId/replace-url', async (req, res) => {
376
+ try {
377
+ const auth = req.auth;
378
+ const { projectId, connId, fileId } = req.params;
379
+ const { contentType, fileName } = req.query;
380
+ if (!contentType || !fileName) {
381
+ return res.status(400).json({ message: 'Query params contentType and fileName are required' });
382
+ }
383
+ const project = await Project.findOne({ _id: projectId });
384
+ if (!project)
385
+ return res.status(404).json({ message: 'Project not found' });
386
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
387
+ return res.status(403).json({ message: 'Unauthorized access' });
388
+ const collection = getRawFilesCollection();
389
+ if (!collection)
390
+ return res.status(500).json({ message: 'Database not available' });
391
+ const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
392
+ if (!fileDoc)
393
+ return res.status(404).json({ message: 'File not found' });
394
+ if (!validateContentType(contentType, fileName, fileDoc.category)) {
395
+ return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
396
+ }
397
+ const connAdapter = await resolveConnAdapter(connId, project, adapter);
398
+ if ('error' in connAdapter)
399
+ return res.status(connAdapter.status).json({ message: connAdapter.error });
400
+ if (!connAdapter.getPresignedUploadUrl) {
401
+ return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
402
+ }
403
+ const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType);
404
+ await collection.updateOne(connFilter(projectId, connId, { fileId }), { $set: { contentType, updatedAt: new Date() } });
405
+ res.json({ url, storageKey: fileDoc.storageKey, fileId });
406
+ }
407
+ catch (err) {
408
+ res.status(500).json({ message: 'Failed to generate replace URL', details: err.message });
409
+ }
410
+ });
342
411
  router.patch('/projects/:projectId/connections/storage/:connId/files/:fileId', async (req, res) => {
343
412
  try {
344
413
  const auth = req.auth;
@@ -16,6 +16,8 @@ const resolveSubscription = async (auth) => {
16
16
  const ensureSubscription = async (req, res, next) => {
17
17
  try {
18
18
  const auth = req.auth;
19
+ if (!auth)
20
+ return next();
19
21
  const plan = await resolveSubscription(auth);
20
22
  req.subscription = plan;
21
23
  next();
@@ -45,11 +45,11 @@ class Model {
45
45
  // Handle members filter for SQL projects
46
46
  const userId = processedFilter.members.$elemMatch.userId;
47
47
  const schema = this.schema || 'streamby'; // Default to 'streamby' if schema not provided
48
- const query = `
49
- SELECT p.*, pm."userId" as memberUserId, pm.role as memberRole, pm.status as memberStatus, pm.archived as memberArchived
50
- FROM "${schema}"."projects" p
51
- JOIN "${schema}"."project_members" pm ON p.id = pm."projectId"
52
- WHERE pm."userId" = $1
48
+ const query = `
49
+ SELECT p.*, pm."userId" as memberUserId, pm.role as memberRole, pm.status as memberStatus, pm.archived as memberArchived
50
+ FROM "${schema}"."projects" p
51
+ JOIN "${schema}"."project_members" pm ON p.id = pm."projectId"
52
+ WHERE pm."userId" = $1
53
53
  `;
54
54
  const result = await connection.query(query, [userId]);
55
55
  sqlResults = result.rows.map(row => ({
@@ -219,10 +219,10 @@ class Model {
219
219
  else {
220
220
  const matchKey = value.id !== undefined ? 'id' : Object.keys(value)[0];
221
221
  const matchVal = String(value[matchKey]);
222
- await connection.query(`UPDATE ${tbl} SET "${field}" = (
223
- SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
224
- FROM jsonb_array_elements("${field}") elem
225
- WHERE elem->>'${matchKey}' != $1
222
+ await connection.query(`UPDATE ${tbl} SET "${field}" = (
223
+ SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
224
+ FROM jsonb_array_elements("${field}") elem
225
+ WHERE elem->>'${matchKey}' != $1
226
226
  ) WHERE id = $2`, [matchVal, id]);
227
227
  }
228
228
  }
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
- {
2
- "name": "streamby-core",
3
- "version": "0.28.1",
4
- "description": "StreamBy middleware framework for media storage management",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist"
9
- ],
10
- "scripts": {
11
- "build": "tsc",
12
- "test": "vitest",
13
- "dev": "ts-node-dev --respawn --clear dev.ts",
14
- "prepublishOnly": "npm run build"
15
- },
16
- "repository": {
17
- "type": "git",
18
- "url": "https://github.com/GwerhDev/StreamBy-Package.git"
19
- },
20
- "keywords": [
21
- "streamby",
22
- "middleware",
23
- "upload",
24
- "storage",
25
- "s3",
26
- "express",
27
- "typescript"
28
- ],
29
- "author": "TerminalCore Labs",
30
- "license": "MIT",
31
- "dependencies": {
32
- "@aws-sdk/client-s3": "^3.528.0",
33
- "@aws-sdk/s3-request-presigner": "^3.787.0",
34
- "@prisma/client": "^6.12.0",
35
- "bcrypt": "^5.1.1",
36
- "dotenv": "^16.5.0",
37
- "express": "^4.0.0",
38
- "mongodb": "^6.17.0",
39
- "mongoose": "^8.13.2",
40
- "node-fetch": "^3.0.0",
41
- "pg": "^8.16.3",
42
- "prisma": "^6.12.0",
43
- "ws": "^8.20.0"
44
- },
45
- "devDependencies": {
46
- "@types/bcrypt": "^5.0.2",
47
- "@types/cors": "^2.8.18",
48
- "@types/express": "^4.17.17",
49
- "@types/mongodb": "^4.0.7",
50
- "@types/node": "^22.15.21",
51
- "@types/pg": "^8.15.4",
52
- "@types/supertest": "^6.0.3",
53
- "@types/ws": "^8.18.1",
54
- "cors": "^2.8.5",
55
- "supertest": "^7.1.4",
56
- "ts-node-dev": "^2.0.0",
57
- "typescript": "^5.8.3",
58
- "vitest": "^3.1.1"
59
- },
60
- "peerDependencies": {
61
- "express": "^4.0.0"
62
- }
63
- }
1
+ {
2
+ "name": "streamby-core",
3
+ "version": "0.28.4",
4
+ "description": "StreamBy middleware framework for media storage management",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest",
13
+ "dev": "ts-node-dev --respawn --clear dev.ts",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/GwerhDev/StreamBy-Package.git"
19
+ },
20
+ "keywords": [
21
+ "streamby",
22
+ "middleware",
23
+ "upload",
24
+ "storage",
25
+ "s3",
26
+ "express",
27
+ "typescript"
28
+ ],
29
+ "author": "TerminalCore Labs",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@aws-sdk/client-s3": "^3.528.0",
33
+ "@aws-sdk/s3-request-presigner": "^3.787.0",
34
+ "@prisma/client": "^6.12.0",
35
+ "bcrypt": "^5.1.1",
36
+ "dotenv": "^16.5.0",
37
+ "express": "^4.0.0",
38
+ "mongodb": "^6.17.0",
39
+ "mongoose": "^8.13.2",
40
+ "node-fetch": "^3.0.0",
41
+ "pg": "^8.16.3",
42
+ "prisma": "^6.12.0",
43
+ "ws": "^8.20.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bcrypt": "^5.0.2",
47
+ "@types/cors": "^2.8.18",
48
+ "@types/express": "^4.17.17",
49
+ "@types/mongodb": "^4.0.7",
50
+ "@types/node": "^22.15.21",
51
+ "@types/pg": "^8.15.4",
52
+ "@types/supertest": "^6.0.3",
53
+ "@types/ws": "^8.18.1",
54
+ "cors": "^2.8.5",
55
+ "supertest": "^7.1.4",
56
+ "ts-node-dev": "^2.0.0",
57
+ "typescript": "^5.8.3",
58
+ "vitest": "^3.1.1"
59
+ },
60
+ "peerDependencies": {
61
+ "express": "^4.0.0"
62
+ }
63
+ }
@@ -1,3 +0,0 @@
1
- export declare function encrypt(text: string, secretKey: string): string;
2
- export declare function decrypt(text: string, secretKey: string): string;
3
- export declare function hash(text: string): string;
@@ -1,32 +0,0 @@
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.encrypt = encrypt;
7
- exports.decrypt = decrypt;
8
- exports.hash = hash;
9
- const crypto_1 = __importDefault(require("crypto"));
10
- const ALGORITHM = 'aes-256-cbc';
11
- const IV_LENGTH = 16;
12
- function encrypt(text, secretKey) {
13
- const iv = crypto_1.default.randomBytes(IV_LENGTH);
14
- const key = crypto_1.default.createHash('sha256').update(String(secretKey)).digest('base64').substr(0, 32);
15
- const cipher = crypto_1.default.createCipheriv(ALGORITHM, Buffer.from(key), iv);
16
- let encrypted = cipher.update(text);
17
- encrypted = Buffer.concat([encrypted, cipher.final()]);
18
- return iv.toString('hex') + ':' + encrypted.toString('hex');
19
- }
20
- function decrypt(text, secretKey) {
21
- const textParts = text.split(':');
22
- const iv = Buffer.from(textParts.shift(), 'hex');
23
- const encryptedText = Buffer.from(textParts.join(':'), 'hex');
24
- const key = crypto_1.default.createHash('sha256').update(String(secretKey)).digest('base64').substr(0, 32);
25
- const decipher = crypto_1.default.createDecipheriv(ALGORITHM, Buffer.from(key), iv);
26
- let decrypted = decipher.update(encryptedText);
27
- decrypted = Buffer.concat([decrypted, decipher.final()]);
28
- return decrypted.toString();
29
- }
30
- function hash(text) {
31
- return crypto_1.default.createHash('sha256').update(text).digest('hex');
32
- }