@progalaxyelabs/stonescriptphp-files 1.0.1 → 2.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@progalaxyelabs/stonescriptphp-files",
3
- "version": "1.0.1",
3
+ "version": "2.0.1",
4
4
  "description": "Zero-config Express file server with Azure Blob Storage and JWT authentication",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -18,7 +18,8 @@
18
18
  "multer": "^1.4.5-lts.1",
19
19
  "jsonwebtoken": "^9.0.2",
20
20
  "cors": "^2.8.5",
21
- "uuid": "^11.0.5"
21
+ "uuid": "^11.0.5",
22
+ "express-rate-limit": "^7.4.0"
22
23
  },
23
24
  "keywords": [
24
25
  "azure",
package/src/auth.js CHANGED
@@ -41,9 +41,13 @@ export function createAuthMiddleware(publicKey) {
41
41
  });
42
42
  }
43
43
 
44
+ // Extract tenant_id from token
45
+ const tenantId = decoded.tenant_id || decoded.tid || decoded.tenant_uuid || null;
46
+
44
47
  // Attach user info to request
45
48
  req.user = {
46
49
  id: userId,
50
+ tenantId,
47
51
  email: decoded.email,
48
52
  roles: decoded.roles || [],
49
53
  ...decoded
@@ -75,6 +79,85 @@ export function createAuthMiddleware(publicKey) {
75
79
  };
76
80
  }
77
81
 
82
+ /**
83
+ * JWKS-based JWT validation middleware factory
84
+ * Creates a middleware that validates JWT tokens using JWKS (JSON Web Key Sets)
85
+ *
86
+ * @param {JwksClient} jwksClient - JWKS client instance for key retrieval
87
+ * @returns {Function} Express middleware function
88
+ */
89
+ export function createJwksAuthMiddleware(jwksClient) {
90
+ if (!jwksClient) {
91
+ throw new Error('JWKS client is required');
92
+ }
93
+
94
+ return async function authenticateJwks(req, res, next) {
95
+ const authHeader = req.headers.authorization;
96
+
97
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
98
+ return res.status(401).json({
99
+ error: 'Unauthorized',
100
+ message: 'Missing or invalid authorization header'
101
+ });
102
+ }
103
+
104
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
105
+
106
+ try {
107
+ // Get signing key from JWKS
108
+ const { key, algorithm } = await jwksClient.getSigningKey(token);
109
+
110
+ // Verify token with the retrieved key
111
+ const decoded = jwt.verify(token, key, { algorithms: [algorithm] });
112
+
113
+ // Extract user_id from token
114
+ const userId = decoded.sub || decoded.user_id || decoded.userId || decoded.id;
115
+
116
+ if (!userId) {
117
+ return res.status(401).json({
118
+ error: 'Unauthorized',
119
+ message: 'Token missing user identifier'
120
+ });
121
+ }
122
+
123
+ // Extract tenant_id from token
124
+ const tenantId = decoded.tenant_id || decoded.tid || decoded.tenant_uuid || null;
125
+
126
+ // Attach user info to request
127
+ req.user = {
128
+ id: userId,
129
+ tenantId,
130
+ email: decoded.email,
131
+ roles: decoded.roles || [],
132
+ ...decoded
133
+ };
134
+
135
+ next();
136
+ } catch (error) {
137
+ console.error('JWKS JWT validation error:', error.message);
138
+
139
+ if (error.name === 'TokenExpiredError') {
140
+ return res.status(401).json({
141
+ error: 'Unauthorized',
142
+ message: 'Token expired'
143
+ });
144
+ }
145
+
146
+ if (error.name === 'JsonWebTokenError') {
147
+ return res.status(401).json({
148
+ error: 'Unauthorized',
149
+ message: 'Invalid token'
150
+ });
151
+ }
152
+
153
+ return res.status(401).json({
154
+ error: 'Unauthorized',
155
+ message: 'Token validation failed'
156
+ });
157
+ }
158
+ };
159
+ }
160
+
78
161
  /**
79
162
  * Optional: Role-based authorization middleware factory
80
163
  * Use after authenticateJWT to check for specific roles
@@ -28,10 +28,8 @@ export class AzureStorageClient {
28
28
  // Get container client
29
29
  this.containerClient = this.blobServiceClient.getContainerClient(this.containerName);
30
30
 
31
- // Create container if it doesn't exist
32
- await this.containerClient.createIfNotExists({
33
- access: 'private' // CRITICAL: No public access
34
- });
31
+ // Create container if it doesn't exist (no access property = private, no public access)
32
+ await this.containerClient.createIfNotExists();
35
33
 
36
34
  console.log(`Azure Blob Storage initialized: container=${this.containerName}`);
37
35
  } catch (error) {
@@ -42,21 +40,23 @@ export class AzureStorageClient {
42
40
 
43
41
  /**
44
42
  * Upload file to Azure Blob Storage
43
+ * @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
45
44
  * @param {string} userId - User ID (for namespacing)
46
45
  * @param {Buffer} fileBuffer - File content
47
46
  * @param {string} originalFilename - Original filename
48
47
  * @param {string} contentType - MIME type
49
48
  * @returns {Object} File metadata
50
49
  */
51
- async uploadFile(userId, fileBuffer, originalFilename, contentType) {
50
+ async uploadFile(tenantId, userId, fileBuffer, originalFilename, contentType) {
52
51
  if (!this.containerClient) {
53
52
  throw new Error('Azure Storage not initialized');
54
53
  }
55
54
 
56
- // Generate unique blob name: {user_id}/{uuid}.{ext}
55
+ // Generate unique blob name with tenant prefix: {tenant_id}/{user_id}/{uuid}.{ext}
57
56
  const fileId = uuidv4();
58
57
  const extension = originalFilename.split('.').pop();
59
- const blobName = `${userId}/${fileId}.${extension}`;
58
+ const prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
59
+ const blobName = `${prefix}${fileId}.${extension}`;
60
60
 
61
61
  const blockBlobClient = this.containerClient.getBlockBlobClient(blobName);
62
62
 
@@ -66,6 +66,7 @@ export class AzureStorageClient {
66
66
  blobContentType: contentType
67
67
  },
68
68
  metadata: {
69
+ tenant_id: tenantId || '',
69
70
  user_id: userId,
70
71
  original_filename: originalFilename,
71
72
  content_type: contentType,
@@ -77,6 +78,7 @@ export class AzureStorageClient {
77
78
  return {
78
79
  fileId,
79
80
  blobName,
81
+ tenantId,
80
82
  userId,
81
83
  originalFilename,
82
84
  contentType,
@@ -88,17 +90,19 @@ export class AzureStorageClient {
88
90
  /**
89
91
  * Download file from Azure Blob Storage
90
92
  * @param {string} fileId - File ID (UUID)
93
+ * @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
91
94
  * @param {string} userId - User ID (for authorization)
92
95
  * @returns {Object} { stream, metadata }
93
96
  */
94
- async downloadFile(fileId, userId) {
97
+ async downloadFile(fileId, tenantId, userId) {
95
98
  if (!this.containerClient) {
96
99
  throw new Error('Azure Storage not initialized');
97
100
  }
98
101
 
99
- // List blobs with file_id metadata to find the blob
102
+ // List blobs with prefix to find the blob
103
+ const prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
100
104
  const blobs = this.containerClient.listBlobsFlat({
101
- prefix: `${userId}/`,
105
+ prefix,
102
106
  includeMetadata: true
103
107
  });
104
108
 
@@ -130,17 +134,19 @@ export class AzureStorageClient {
130
134
 
131
135
  /**
132
136
  * List user's files
137
+ * @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
133
138
  * @param {string} userId - User ID
134
139
  * @returns {Array} List of file metadata
135
140
  */
136
- async listFiles(userId) {
141
+ async listFiles(tenantId, userId) {
137
142
  if (!this.containerClient) {
138
143
  throw new Error('Azure Storage not initialized');
139
144
  }
140
145
 
141
146
  const files = [];
147
+ const prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
142
148
  const blobs = this.containerClient.listBlobsFlat({
143
- prefix: `${userId}/`,
149
+ prefix,
144
150
  includeMetadata: true
145
151
  });
146
152
 
@@ -162,16 +168,18 @@ export class AzureStorageClient {
162
168
  /**
163
169
  * Delete file from Azure Blob Storage
164
170
  * @param {string} fileId - File ID (UUID)
171
+ * @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
165
172
  * @param {string} userId - User ID (for authorization)
166
173
  */
167
- async deleteFile(fileId, userId) {
174
+ async deleteFile(fileId, tenantId, userId) {
168
175
  if (!this.containerClient) {
169
176
  throw new Error('Azure Storage not initialized');
170
177
  }
171
178
 
172
179
  // Find blob by file_id
180
+ const prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
173
181
  const blobs = this.containerClient.listBlobsFlat({
174
- prefix: `${userId}/`,
182
+ prefix,
175
183
  includeMetadata: true
176
184
  });
177
185
 
package/src/index.js CHANGED
@@ -1,13 +1,45 @@
1
1
  import express from 'express';
2
2
  import cors from 'cors';
3
3
  import { AzureStorageClient } from './azure-storage.js';
4
- import { createAuthMiddleware } from './auth.js';
4
+ import { createAuthMiddleware, createJwksAuthMiddleware } from './auth.js';
5
+ import { JwksClient } from './jwks-client.js';
6
+ import { createRateLimiters } from './rate-limit.js';
5
7
  import { createUploadRouter } from './routes/upload.js';
6
8
  import { createDownloadRouter } from './routes/download.js';
7
9
  import { createListRouter } from './routes/list.js';
8
10
  import { createDeleteRouter } from './routes/delete.js';
9
11
  import { createHealthRouter } from './routes/health.js';
10
12
 
13
+ /**
14
+ * Resolve the authentication middleware based on configuration priority.
15
+ * Priority: authServers > jwksUrl > jwtPublicKey
16
+ *
17
+ * @param {Object} config - Configuration options
18
+ * @returns {Function} Express middleware function for authentication
19
+ */
20
+ function resolveAuthMiddleware(config) {
21
+ const authServersJson = config.authServers || (process.env.AUTH_SERVERS ? JSON.parse(process.env.AUTH_SERVERS) : null);
22
+ const jwksUrl = config.jwksUrl || process.env.JWKS_URL;
23
+ const jwtPublicKey = config.jwtPublicKey || process.env.JWT_PUBLIC_KEY;
24
+ const cacheTtl = config.jwksCacheTtl || parseInt(process.env.JWKS_CACHE_TTL) || 3600;
25
+
26
+ if (authServersJson) {
27
+ const jwksClient = new JwksClient(authServersJson.map(s => ({ ...s, cacheTtl })));
28
+ return createJwksAuthMiddleware(jwksClient);
29
+ }
30
+
31
+ if (jwksUrl) {
32
+ const jwksClient = new JwksClient([{ issuer: '*', jwksUrl, cacheTtl }]);
33
+ return createJwksAuthMiddleware(jwksClient);
34
+ }
35
+
36
+ if (jwtPublicKey) {
37
+ return createAuthMiddleware(jwtPublicKey);
38
+ }
39
+
40
+ throw new Error('No authentication method configured. Set AUTH_SERVERS, JWKS_URL, or JWT_PUBLIC_KEY.');
41
+ }
42
+
11
43
  /**
12
44
  * Create a configured files server
13
45
  * @param {Object} config - Configuration options
@@ -15,23 +47,33 @@ import { createHealthRouter } from './routes/health.js';
15
47
  * @param {string} config.containerName - Azure container name (default: process.env.AZURE_CONTAINER_NAME || 'platform-files')
16
48
  * @param {string} config.azureConnectionString - Azure storage connection string (default: process.env.AZURE_STORAGE_CONNECTION_STRING)
17
49
  * @param {string} config.jwtPublicKey - JWT public key for auth (default: process.env.JWT_PUBLIC_KEY)
50
+ * @param {Array} config.authServers - Array of auth server configs [{issuer, jwksUrl, cacheTtl?}]
51
+ * @param {string} config.jwksUrl - Single JWKS URL for key retrieval
52
+ * @param {number} config.jwksCacheTtl - JWKS cache TTL in seconds (default: 3600)
53
+ * @param {boolean} config.tenantScoped - Enable tenant-scoped file isolation (default: true)
18
54
  * @param {number} config.maxFileSize - Max file size in bytes (default: 100MB)
19
55
  * @param {string|string[]} config.corsOrigins - CORS allowed origins (default: '*')
56
+ * @param {number} config.rateLimitWindowMs - Rate limit window in ms (default: 60000)
57
+ * @param {number} config.rateLimitUpload - Max uploads per window (default: 10)
58
+ * @param {number} config.rateLimitDownload - Max downloads per window (default: 60)
20
59
  * @returns {Object} Server object with app, storage, and listen() method
21
60
  */
22
61
  export function createFilesServer(config = {}) {
23
62
  const port = config.port || process.env.PORT || 3000;
24
63
  const containerName = config.containerName || process.env.AZURE_CONTAINER_NAME || 'platform-files';
25
64
  const connectionString = config.azureConnectionString || process.env.AZURE_STORAGE_CONNECTION_STRING;
26
- const jwtPublicKey = config.jwtPublicKey || process.env.JWT_PUBLIC_KEY;
27
65
  const maxFileSize = config.maxFileSize || 100 * 1024 * 1024;
28
66
  const corsOrigins = config.corsOrigins || '*';
67
+ const tenantScoped = config.tenantScoped !== undefined ? config.tenantScoped : (process.env.TENANT_SCOPED !== 'false');
29
68
 
30
69
  // Create storage client
31
70
  const storage = new AzureStorageClient(connectionString, containerName);
32
71
 
33
- // Create auth middleware
34
- const authenticate = createAuthMiddleware(jwtPublicKey);
72
+ // Resolve auth middleware based on config priority
73
+ const authenticate = resolveAuthMiddleware(config);
74
+
75
+ // Create rate limiters
76
+ const { uploadLimiter, downloadLimiter } = createRateLimiters(config);
35
77
 
36
78
  // Create Express app
37
79
  const app = express();
@@ -49,12 +91,12 @@ export function createFilesServer(config = {}) {
49
91
  next();
50
92
  });
51
93
 
52
- // Routes
94
+ // Routes with rate limiters
53
95
  app.use(createHealthRouter());
54
- app.use(authenticate, createUploadRouter(storage, maxFileSize));
55
- app.use(authenticate, createDownloadRouter(storage));
56
- app.use(authenticate, createListRouter(storage));
57
- app.use(authenticate, createDeleteRouter(storage));
96
+ app.use(authenticate, uploadLimiter, createUploadRouter(storage, maxFileSize));
97
+ app.use(authenticate, downloadLimiter, createDownloadRouter(storage));
98
+ app.use(authenticate, downloadLimiter, createListRouter(storage));
99
+ app.use(authenticate, downloadLimiter, createDeleteRouter(storage));
58
100
 
59
101
  // 404 handler
60
102
  app.use((req, res) => {
@@ -106,7 +148,9 @@ export function createFilesServer(config = {}) {
106
148
 
107
149
  // Named exports for advanced/composable usage
108
150
  export { AzureStorageClient } from './azure-storage.js';
109
- export { createAuthMiddleware } from './auth.js';
151
+ export { createAuthMiddleware, createJwksAuthMiddleware } from './auth.js';
152
+ export { JwksClient } from './jwks-client.js';
153
+ export { createRateLimiters } from './rate-limit.js';
110
154
  export { createUploadRouter } from './routes/upload.js';
111
155
  export { createDownloadRouter } from './routes/download.js';
112
156
  export { createListRouter } from './routes/list.js';
@@ -0,0 +1,59 @@
1
+ import crypto from 'crypto';
2
+ import jwt from 'jsonwebtoken';
3
+
4
+ export class JwksClient {
5
+ constructor(authServers) {
6
+ // authServers: [{issuer, jwksUrl, cacheTtl?}]
7
+ this.authServers = authServers;
8
+ this.cache = new Map(); // key: issuerKey, value: {jwks, fetchedAt}
9
+ this.defaultCacheTtl = 3600 * 1000; // 1 hour in ms
10
+ }
11
+
12
+ async getSigningKey(token) {
13
+ // 1. Decode token header (without verification) to get kid and iss
14
+ const decoded = jwt.decode(token, { complete: true });
15
+ if (!decoded) throw new Error('Invalid token');
16
+
17
+ const { kid, alg } = decoded.header;
18
+ const iss = decoded.payload.iss;
19
+
20
+ // 2. Find matching auth server by issuer (or use first if iss is '*' or not found)
21
+ const server = this.authServers.find(s => s.issuer === iss || s.issuer === '*') || this.authServers[0];
22
+ if (!server) throw new Error('No auth server configured for issuer: ' + iss);
23
+
24
+ // 3. Fetch JWKS (cached)
25
+ const jwks = await this.fetchJwks(server.issuer, server);
26
+
27
+ // 4. Find key by kid
28
+ const jwk = jwks.keys.find(k => k.kid === kid);
29
+ if (!jwk) throw new Error('Signing key not found for kid: ' + kid);
30
+
31
+ // 5. Convert JWK to PEM
32
+ const key = this.jwkToPem(jwk);
33
+
34
+ return { key, algorithm: alg || 'RS256' };
35
+ }
36
+
37
+ async fetchJwks(issuerKey, serverConfig) {
38
+ const cacheTtl = (serverConfig.cacheTtl || this.defaultCacheTtl / 1000) * 1000;
39
+ const cached = this.cache.get(issuerKey);
40
+
41
+ if (cached && (Date.now() - cached.fetchedAt) < cacheTtl) {
42
+ return cached.jwks;
43
+ }
44
+
45
+ const response = await fetch(serverConfig.jwksUrl);
46
+ if (!response.ok) {
47
+ throw new Error('Failed to fetch JWKS from ' + serverConfig.jwksUrl + ': ' + response.status);
48
+ }
49
+
50
+ const jwks = await response.json();
51
+ this.cache.set(issuerKey, { jwks, fetchedAt: Date.now() });
52
+
53
+ return jwks;
54
+ }
55
+
56
+ jwkToPem(jwk) {
57
+ return crypto.createPublicKey({ key: jwk, format: 'jwk' });
58
+ }
59
+ }
@@ -0,0 +1,29 @@
1
+ import rateLimit from 'express-rate-limit';
2
+
3
+ export function createRateLimiters(config = {}) {
4
+ const windowMs = config.rateLimitWindowMs || parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60 * 1000;
5
+ const uploadMax = config.rateLimitUpload || parseInt(process.env.RATE_LIMIT_UPLOAD) || 10;
6
+ const downloadMax = config.rateLimitDownload || parseInt(process.env.RATE_LIMIT_DOWNLOAD) || 60;
7
+
8
+ const keyGenerator = (req) => req.user?.id || req.ip;
9
+
10
+ const uploadLimiter = rateLimit({
11
+ windowMs,
12
+ max: uploadMax,
13
+ keyGenerator,
14
+ standardHeaders: true,
15
+ legacyHeaders: false,
16
+ message: { error: 'Too Many Requests', message: 'Upload rate limit exceeded. Try again later.' }
17
+ });
18
+
19
+ const downloadLimiter = rateLimit({
20
+ windowMs,
21
+ max: downloadMax,
22
+ keyGenerator,
23
+ standardHeaders: true,
24
+ legacyHeaders: false,
25
+ message: { error: 'Too Many Requests', message: 'Rate limit exceeded. Try again later.' }
26
+ });
27
+
28
+ return { uploadLimiter, downloadLimiter };
29
+ }
@@ -18,6 +18,7 @@ export function createDeleteRouter(storage) {
18
18
  try {
19
19
  const fileId = req.params.id;
20
20
  const userId = req.user.id;
21
+ const tenantId = req.user.tenantId;
21
22
 
22
23
  // Validate file ID format (UUID)
23
24
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -29,7 +30,7 @@ export function createDeleteRouter(storage) {
29
30
  }
30
31
 
31
32
  // Delete file from Azure (validates ownership)
32
- await storage.deleteFile(fileId, userId);
33
+ await storage.deleteFile(fileId, tenantId, userId);
33
34
 
34
35
  res.status(200).json({
35
36
  success: true,
@@ -18,6 +18,7 @@ export function createDownloadRouter(storage) {
18
18
  try {
19
19
  const fileId = req.params.id;
20
20
  const userId = req.user.id;
21
+ const tenantId = req.user.tenantId;
21
22
 
22
23
  // Validate file ID format (UUID)
23
24
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -29,7 +30,7 @@ export function createDownloadRouter(storage) {
29
30
  }
30
31
 
31
32
  // Download file from Azure (validates ownership)
32
- const { stream, metadata } = await storage.downloadFile(fileId, userId);
33
+ const { stream, metadata } = await storage.downloadFile(fileId, tenantId, userId);
33
34
 
34
35
  // Set response headers
35
36
  res.setHeader('Content-Type', metadata.contentType);
@@ -17,9 +17,10 @@ export function createListRouter(storage) {
17
17
  router.get('/files', async (req, res) => {
18
18
  try {
19
19
  const userId = req.user.id;
20
+ const tenantId = req.user.tenantId;
20
21
 
21
22
  // List files for current user
22
- const files = await storage.listFiles(userId);
23
+ const files = await storage.listFiles(tenantId, userId);
23
24
 
24
25
  res.status(200).json({
25
26
  success: true,
@@ -41,21 +41,30 @@ export function createUploadRouter(storage, maxFileSize = 100 * 1024 * 1024) {
41
41
  }
42
42
 
43
43
  const userId = req.user.id;
44
+ const tenantId = req.user.tenantId;
44
45
  const { buffer, originalname, mimetype } = req.file;
45
46
 
46
47
  // Upload to Azure Blob Storage
47
- const fileMetadata = await storage.uploadFile(userId, buffer, originalname, mimetype);
48
+ const fileMetadata = await storage.uploadFile(tenantId, userId, buffer, originalname, mimetype);
49
+
50
+ // Build response
51
+ const fileResponse = {
52
+ id: fileMetadata.fileId,
53
+ name: fileMetadata.originalFilename,
54
+ contentType: fileMetadata.contentType,
55
+ size: fileMetadata.size,
56
+ uploadedAt: fileMetadata.uploadedAt
57
+ };
58
+
59
+ // Include tenantId in response if present
60
+ if (tenantId) {
61
+ fileResponse.tenantId = tenantId;
62
+ }
48
63
 
49
64
  // Return success response
50
65
  res.status(201).json({
51
66
  success: true,
52
- file: {
53
- id: fileMetadata.fileId,
54
- name: fileMetadata.originalFilename,
55
- contentType: fileMetadata.contentType,
56
- size: fileMetadata.size,
57
- uploadedAt: fileMetadata.uploadedAt
58
- }
67
+ file: fileResponse
59
68
  });
60
69
  } catch (error) {
61
70
  console.error('Upload error:', error);
package/HLD.md DELETED
@@ -1,170 +0,0 @@
1
- # @progalaxyelabs/stonescriptphp-files — High Level Design
2
-
3
- **Version**: 1.0.0
4
- **Last Updated**: 2026-02-07
5
-
6
- ## Overview
7
-
8
- A shared npm package that provides a ready-to-use Express file server with Azure Blob Storage and JWT authentication.
9
-
10
- ### Developer Experience
11
- ```bash
12
- mkdir files && cd files
13
- npm init -y
14
- npm i express @progalaxyelabs/stonescriptphp-files
15
- ```
16
-
17
- ```js
18
- // index.js
19
- import 'dotenv/config';
20
- import { createFilesServer } from '@progalaxyelabs/stonescriptphp-files';
21
- createFilesServer().listen();
22
- ```
23
-
24
- ## Architecture
25
-
26
- ### Package Exports
27
-
28
- ```
29
- @progalaxyelabs/stonescriptphp-files
30
- ├── createFilesServer(config?) // Factory: returns configured Express app
31
- ├── AzureStorageClient // Class: Azure Blob Storage operations
32
- ├── createAuthMiddleware // Middleware factory: JWT Bearer token validation
33
- ├── createUploadRouter // Express Router factory: POST /upload
34
- ├── createDownloadRouter // Express Router factory: GET /files/:id
35
- ├── createListRouter // Express Router factory: GET /files
36
- ├── createDeleteRouter // Express Router factory: DELETE /files/:id
37
- └── createHealthRouter // Express Router factory: GET /health
38
- ```
39
-
40
- ### `createFilesServer(config?)` Factory
41
-
42
- ```js
43
- createFilesServer({
44
- port: process.env.PORT || 3000,
45
- containerName: process.env.AZURE_CONTAINER_NAME || 'platform-files',
46
- azureConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
47
- jwtPublicKey: process.env.JWT_PUBLIC_KEY,
48
- maxFileSize: 100 * 1024 * 1024, // 100MB default
49
- corsOrigins: '*',
50
- });
51
- ```
52
-
53
- All config is optional — defaults to environment variables so the minimal setup works with zero arguments.
54
-
55
- ### Request Flow
56
-
57
- ```
58
- Client
59
-
60
- ├─ POST /upload (multipart/form-data)
61
- │ → JWT auth middleware
62
- │ → multer (memory storage)
63
- │ → AzureStorageClient.uploadFile()
64
- │ → Returns { success, file: { id, name, size, contentType, uploadedAt } }
65
-
66
- ├─ GET /files/:id
67
- │ → JWT auth middleware
68
- │ → AzureStorageClient.downloadFile()
69
- │ → Streams blob to response (Content-Type, Content-Disposition)
70
-
71
- ├─ GET /files
72
- │ → JWT auth middleware
73
- │ → AzureStorageClient.listFiles(userId)
74
- │ → Returns { success, count, files: [...] }
75
-
76
- ├─ DELETE /files/:id
77
- │ → JWT auth middleware
78
- │ → AzureStorageClient.deleteFile()
79
- │ → Returns { success, message }
80
-
81
- └─ GET /health
82
- → No auth
83
- → Returns { status: "healthy", service: "files-service", timestamp }
84
- ```
85
-
86
- ## Components
87
-
88
- ### createFilesServer (factory)
89
- Main export. Creates and returns a configured Express app with all routes, middleware, and graceful shutdown. Accepts optional config object, defaults to env vars.
90
-
91
- ### AzureStorageClient (class)
92
- Wraps `@azure/storage-blob` SDK. Methods: `initialize()`, `uploadFile(userId, buffer, filename, contentType)`, `downloadFile(fileId, userId)`, `listFiles(userId)`, `deleteFile(fileId, userId)`. Auto-creates container on init with private access.
93
-
94
- ### createAuthMiddleware (middleware factory)
95
- Express middleware factory. Validates Bearer token, extracts user identity from standard JWT claims (`sub`, `user_id`, `userId`, `id`), attaches to `req.user`. Supports RS256/ES256/HS256.
96
-
97
- ### Route handlers (router factories)
98
- Express Router factory functions for upload, download, list, delete, health. Can be used individually for custom mounting or composed via `createFilesServer`.
99
-
100
- ## Tech Stack
101
-
102
- | Component | Technology |
103
- |-----------|-----------|
104
- | Runtime | Node.js 24+ |
105
- | Framework | Express 5.x (peerDependency) |
106
- | Language | JavaScript (ESM) |
107
- | Storage | Azure Blob Storage (`@azure/storage-blob` ^12.24.0) |
108
- | Auth | JWT (`jsonwebtoken` ^9.0.2) |
109
- | Upload | `multer` ^1.4.5-lts.1 |
110
- | Module type | ESM (`"type": "module"`) |
111
-
112
- ## Storage Design
113
-
114
- - **SDK:** `@azure/storage-blob` v12.x
115
- - **Auth:** Connection string (env var)
116
- - **Container:** auto-created on startup with private access
117
- - **Blob path:** `{userId}/{uuid}.{extension}`
118
- - **Metadata:** original_name, user_id, content_type, uploaded_at, file_id
119
-
120
- ## JWT Authentication
121
-
122
- - **Algorithms:** RS256, ES256, HS256
123
- - **Token source:** `Authorization: Bearer <token>` header
124
- - **User ID extraction:** `sub` | `user_id` | `userId` | `id` claim
125
- - **Ownership:** files are scoped to the authenticated user's ID
126
- - **Health endpoint:** excluded from auth
127
- - **Role-based auth:** optional `requireRole()` middleware export
128
-
129
- ## Environment Variables
130
-
131
- | Variable | Required | Default | Description |
132
- |----------|----------|---------|-------------|
133
- | `AZURE_STORAGE_CONNECTION_STRING` | Yes | — | Azure Storage connection string |
134
- | `AZURE_CONTAINER_NAME` | No | `platform-files` | Blob container name |
135
- | `JWT_PUBLIC_KEY` | Yes | — | Public key for JWT verification |
136
- | `PORT` | No | `3000` | Server listen port |
137
-
138
- ## Requirements
139
-
140
- ### Functional
141
- - Upload files via multipart/form-data to Azure Blob Storage
142
- - Download files with ownership verification and proper headers
143
- - List all files belonging to the authenticated user
144
- - Delete files with ownership verification
145
- - JWT Bearer token authentication on all file operations
146
- - Health check endpoint (unauthenticated)
147
- - Auto-create Azure Blob container on startup
148
- - Graceful shutdown on SIGTERM/SIGINT
149
- - 100MB default file size limit (configurable)
150
- - CORS support (configurable origins)
151
-
152
- ### Non-Functional
153
- - Zero-config startup (all settings from env vars)
154
- - No platform-specific code — fully generic and reusable
155
- - ESM module (`"type": "module"`)
156
- - Node.js 24+ engine requirement
157
- - Express 5.x as peer dependency
158
-
159
- ### Developer Experience
160
- - Setup in 3 commands: `npm init`, `npm i express @progalaxyelabs/stonescriptphp-files`, create index.js
161
- - Consumer service has only 2 dependencies: `express` and this package
162
- - All config via env vars with sensible defaults
163
- - Composable exports for advanced/custom usage
164
-
165
- ## Constraints
166
-
167
- - Must work with any JWT issuer (RS256/ES256/HS256)
168
- - No breaking changes to Docker environment variables
169
- - Published to npm as `@progalaxyelabs/stonescriptphp-files` (scoped, public)
170
- - Source: https://github.com/progalaxyelabs/stonescriptphp-files
package/project-info.yaml DELETED
@@ -1,119 +0,0 @@
1
- # project-info.yaml
2
- # Human-readable project metadata and configuration
3
-
4
- # REQUIRED FIELDS
5
- # ===============
6
-
7
- # Project code in format: XXXX-NNN
8
- project_code: "STON-003"
9
-
10
- # Project name (matches directory name)
11
- name: "stonescriptphp-files"
12
-
13
- # Project type: platform | interconnected | standalone
14
- type: "standalone"
15
-
16
- # Project status: production | active-dev | staging | maintenance | paused | archived | planning | unknown
17
- status: "production"
18
-
19
-
20
- # BASIC METADATA
21
- # ==============
22
-
23
- # Human-readable project title
24
- title: "@progalaxyelabs/stonescriptphp-files"
25
-
26
- # Brief project description
27
- description: "Zero-config Express file server with Azure Blob Storage and JWT authentication"
28
-
29
- # Project version (semantic versioning recommended)
30
- version: "1.0.0"
31
-
32
- # Creation date (ISO 8601 format: YYYY-MM-DD)
33
- created: "2026-02-07"
34
-
35
- # Last modified date (ISO 8601 format: YYYY-MM-DD)
36
- last_modified: "2026-02-07"
37
-
38
-
39
- # BUSINESS CLASSIFICATION
40
- # =======================
41
-
42
- # Domain category
43
- domain: "tools"
44
-
45
- # Project purpose: product | client-project | internal-tool | poc | experiment | library | marketing | unknown
46
- purpose: "library"
47
-
48
- # Business model: b2b-saas | b2c-saas | b2b-one-off | b2c-one-off | marketplace | internal | unknown
49
- business_model: "internal"
50
-
51
- # Ownership: progalaxy | client | partner | open-source | unknown
52
- ownership: "open-source"
53
-
54
- # Revenue priority: p0-critical | p1-high | p2-medium | p3-low | p4-none | unknown
55
- revenue_priority: "p4-none"
56
-
57
-
58
- # TECHNICAL DETAILS
59
- # =================
60
-
61
- # Primary technology stack
62
- tech_stack:
63
- language: "JavaScript (ESM)"
64
- runtime: "Node.js >= 24"
65
- framework: "Express 5 (peerDependency)"
66
- storage: "Azure Blob Storage"
67
- auth: "JWT (RS256/ES256/HS256)"
68
-
69
- # Key dependencies
70
- dependencies:
71
- - "@azure/storage-blob"
72
- - "multer"
73
- - "jsonwebtoken"
74
- - "cors"
75
- - "uuid"
76
-
77
- # Deployment tier: tier-0-static | tier-1-simple | tier-2-database | tier-3-services | tier-4-distributed | library | unknown
78
- deployment_tier: "library"
79
-
80
-
81
- # GIT & REPOSITORY
82
- # ================
83
-
84
- repository:
85
- type: "git"
86
- url: "git@github.com:progalaxyelabs/stonescriptphp-files.git"
87
- branch: "main"
88
-
89
- # Package registry
90
- npm:
91
- name: "@progalaxyelabs/stonescriptphp-files"
92
- scope: "progalaxyelabs"
93
- access: "public"
94
-
95
-
96
- # CONSUMERS
97
- # =========
98
-
99
- consumers:
100
- - "progalaxyelabs-platform/files"
101
- - "progalaxy-platform/files"
102
- - "btechrecruiter-platform/files"
103
- - "instituteapp-platform/files"
104
- - "restrantapp-platform/files"
105
- - "medstoreapp-platform/files"
106
- - "logisticsapp-platform/files"
107
- - "specialcomputers-platform/files"
108
- - "aasaanwork-platform/files"
109
- - "webmeteor-platform/files"
110
- - "emcircuitsystems-platform/files"
111
-
112
-
113
- # NOTES
114
- # =====
115
-
116
- notes: |
117
- Factory pattern: createFilesServer(config?) with env var defaults.
118
- Composable exports for advanced usage.
119
- HLD in HLD.md.