@bloomneo/appkit 1.5.1 → 1.5.2
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/AGENTS.md +195 -0
- package/CHANGELOG.md +253 -0
- package/README.md +147 -799
- package/bin/commands/generate.js +7 -7
- package/cookbook/README.md +26 -0
- package/cookbook/api-key-service.ts +106 -0
- package/cookbook/auth-protected-crud.ts +112 -0
- package/cookbook/file-upload-pipeline.ts +113 -0
- package/cookbook/multi-tenant-saas.ts +87 -0
- package/cookbook/real-time-chat.ts +121 -0
- package/dist/auth/auth.d.ts +21 -4
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/auth/auth.js +56 -44
- package/dist/auth/auth.js.map +1 -1
- package/dist/auth/defaults.d.ts +1 -1
- package/dist/auth/defaults.js +35 -35
- package/dist/cache/cache.d.ts +29 -6
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +72 -44
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/defaults.js +25 -25
- package/dist/cache/index.d.ts +19 -10
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +21 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +8 -8
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.js +2 -2
- package/dist/database/adapters/prisma.js +2 -2
- package/dist/database/defaults.d.ts +1 -1
- package/dist/database/defaults.js +4 -4
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/email/defaults.js +20 -20
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +12 -12
- package/dist/error/error.d.ts +12 -0
- package/dist/error/error.d.ts.map +1 -1
- package/dist/error/error.js +19 -0
- package/dist/error/error.js.map +1 -1
- package/dist/error/index.d.ts +14 -3
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +14 -3
- package/dist/error/index.js.map +1 -1
- package/dist/event/defaults.js +30 -30
- package/dist/logger/defaults.d.ts +1 -1
- package/dist/logger/defaults.js +40 -40
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/logger.d.ts +8 -0
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/logger.js +13 -3
- package/dist/logger/logger.js.map +1 -1
- package/dist/logger/transports/console.js +1 -1
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +1 -1
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +1 -1
- package/dist/queue/defaults.d.ts +2 -2
- package/dist/queue/defaults.js +38 -38
- package/dist/security/defaults.d.ts +1 -1
- package/dist/security/defaults.js +29 -29
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +3 -3
- package/dist/security/security.d.ts +1 -1
- package/dist/security/security.js +4 -4
- package/dist/storage/defaults.js +19 -19
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +34 -34
- package/dist/util/env.d.ts +35 -0
- package/dist/util/env.d.ts.map +1 -0
- package/dist/util/env.js +50 -0
- package/dist/util/env.js.map +1 -0
- package/dist/util/errors.d.ts +52 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +82 -0
- package/dist/util/errors.js.map +1 -0
- package/examples/.env.example +80 -0
- package/examples/README.md +16 -0
- package/examples/auth.ts +228 -0
- package/examples/cache.ts +36 -0
- package/examples/config.ts +45 -0
- package/examples/database.ts +69 -0
- package/examples/email.ts +53 -0
- package/examples/error.ts +50 -0
- package/examples/event.ts +42 -0
- package/examples/logger.ts +41 -0
- package/examples/queue.ts +58 -0
- package/examples/security.ts +46 -0
- package/examples/storage.ts +44 -0
- package/examples/util.ts +47 -0
- package/llms.txt +591 -0
- package/package.json +19 -10
- package/src/auth/README.md +850 -0
- package/src/cache/README.md +756 -0
- package/src/config/README.md +604 -0
- package/src/database/README.md +818 -0
- package/src/email/README.md +759 -0
- package/src/error/README.md +660 -0
- package/src/event/README.md +729 -0
- package/src/logger/README.md +435 -0
- package/src/queue/README.md +851 -0
- package/src/security/README.md +612 -0
- package/src/storage/README.md +1008 -0
- package/src/util/README.md +955 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
package/dist/queue/defaults.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* @module @bloomneo/appkit/queue
|
|
4
4
|
* @file src/queue/defaults.ts
|
|
5
5
|
*
|
|
6
|
-
* @llm-rule WHEN: App startup - need to parse
|
|
6
|
+
* @llm-rule WHEN: App startup - need to parse BLOOM_QUEUE_* environment variables and detect transports
|
|
7
7
|
* @llm-rule AVOID: Calling multiple times - expensive validation, use lazy loading in get()
|
|
8
8
|
* @llm-rule NOTE: Called once at startup, cached globally for performance like auth/logging modules
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* Get smart defaults using direct
|
|
11
|
+
* Get smart defaults using direct BLOOM_QUEUE_* environment access
|
|
12
12
|
* @llm-rule WHEN: App startup to get production-ready queue configuration
|
|
13
13
|
* @llm-rule AVOID: Calling repeatedly - validates environment each time, expensive operation
|
|
14
14
|
* @llm-rule NOTE: Called once at startup, cached globally for performance
|
|
@@ -26,44 +26,44 @@ export function getSmartDefaults() {
|
|
|
26
26
|
return {
|
|
27
27
|
transport,
|
|
28
28
|
// Core settings - direct env access
|
|
29
|
-
concurrency: parseInt(process.env.
|
|
30
|
-
maxAttempts: parseInt(process.env.
|
|
31
|
-
retryDelay: parseInt(process.env.
|
|
32
|
-
retryBackoff: process.env.
|
|
29
|
+
concurrency: parseInt(process.env.BLOOM_QUEUE_CONCURRENCY || (isProduction ? '10' : '5')),
|
|
30
|
+
maxAttempts: parseInt(process.env.BLOOM_QUEUE_MAX_ATTEMPTS || '3'),
|
|
31
|
+
retryDelay: parseInt(process.env.BLOOM_QUEUE_RETRY_DELAY || '5000'),
|
|
32
|
+
retryBackoff: process.env.BLOOM_QUEUE_RETRY_BACKOFF || 'exponential',
|
|
33
33
|
// Job management - direct env access
|
|
34
|
-
defaultPriority: parseInt(process.env.
|
|
35
|
-
removeOnComplete: parseInt(process.env.
|
|
36
|
-
removeOnFail: parseInt(process.env.
|
|
34
|
+
defaultPriority: parseInt(process.env.BLOOM_QUEUE_DEFAULT_PRIORITY || '0'),
|
|
35
|
+
removeOnComplete: parseInt(process.env.BLOOM_QUEUE_REMOVE_COMPLETE || (isProduction ? '100' : '10')),
|
|
36
|
+
removeOnFail: parseInt(process.env.BLOOM_QUEUE_REMOVE_FAILED || (isProduction ? '500' : '50')),
|
|
37
37
|
// Memory transport config - direct env access
|
|
38
38
|
memory: {
|
|
39
|
-
maxJobs: parseInt(process.env.
|
|
40
|
-
cleanupInterval: parseInt(process.env.
|
|
39
|
+
maxJobs: parseInt(process.env.BLOOM_QUEUE_MEMORY_MAX_JOBS || (isDevelopment ? '1000' : '100')),
|
|
40
|
+
cleanupInterval: parseInt(process.env.BLOOM_QUEUE_MEMORY_CLEANUP || '30000'),
|
|
41
41
|
},
|
|
42
42
|
// Redis transport config - direct env access
|
|
43
43
|
redis: {
|
|
44
44
|
url: process.env.REDIS_URL || null,
|
|
45
|
-
keyPrefix: process.env.
|
|
46
|
-
maxRetriesPerRequest: parseInt(process.env.
|
|
47
|
-
retryDelayOnFailover: parseInt(process.env.
|
|
45
|
+
keyPrefix: process.env.BLOOM_QUEUE_REDIS_PREFIX || 'queue',
|
|
46
|
+
maxRetriesPerRequest: parseInt(process.env.BLOOM_QUEUE_REDIS_RETRIES || '3'),
|
|
47
|
+
retryDelayOnFailover: parseInt(process.env.BLOOM_QUEUE_REDIS_FAILOVER_DELAY || '100'),
|
|
48
48
|
},
|
|
49
49
|
// Database transport config - direct env access
|
|
50
50
|
database: {
|
|
51
51
|
url: process.env.DATABASE_URL || null,
|
|
52
|
-
tableName: process.env.
|
|
53
|
-
batchSize: parseInt(process.env.
|
|
54
|
-
pollInterval: parseInt(process.env.
|
|
52
|
+
tableName: process.env.BLOOM_QUEUE_DB_TABLE || 'queue_jobs',
|
|
53
|
+
batchSize: parseInt(process.env.BLOOM_QUEUE_DB_BATCH || '50'),
|
|
54
|
+
pollInterval: parseInt(process.env.BLOOM_QUEUE_DB_POLL || (isProduction ? '5000' : '2000')),
|
|
55
55
|
},
|
|
56
56
|
// Worker config - direct env access
|
|
57
57
|
worker: {
|
|
58
58
|
enabled: workerEnabled,
|
|
59
|
-
gracefulShutdownTimeout: parseInt(process.env.
|
|
60
|
-
stalledInterval: parseInt(process.env.
|
|
61
|
-
maxStalledCount: parseInt(process.env.
|
|
59
|
+
gracefulShutdownTimeout: parseInt(process.env.BLOOM_QUEUE_SHUTDOWN_TIMEOUT || '30000'),
|
|
60
|
+
stalledInterval: parseInt(process.env.BLOOM_QUEUE_STALLED_INTERVAL || '30000'),
|
|
61
|
+
maxStalledCount: parseInt(process.env.BLOOM_QUEUE_MAX_STALLED || '1'),
|
|
62
62
|
},
|
|
63
63
|
// Service identification - direct env access
|
|
64
64
|
service: {
|
|
65
|
-
name: process.env.
|
|
66
|
-
version: process.env.
|
|
65
|
+
name: process.env.BLOOM_SERVICE_NAME || process.env.npm_package_name || 'app',
|
|
66
|
+
version: process.env.BLOOM_SERVICE_VERSION || process.env.npm_package_version || '1.0.0',
|
|
67
67
|
environment: nodeEnv,
|
|
68
68
|
},
|
|
69
69
|
};
|
|
@@ -75,7 +75,7 @@ export function getSmartDefaults() {
|
|
|
75
75
|
*/
|
|
76
76
|
function getTransport() {
|
|
77
77
|
// Manual override wins (like auth module pattern)
|
|
78
|
-
const manual = process.env.
|
|
78
|
+
const manual = process.env.BLOOM_QUEUE_TRANSPORT?.toLowerCase();
|
|
79
79
|
if (manual === 'memory' || manual === 'redis' || manual === 'database') {
|
|
80
80
|
return manual;
|
|
81
81
|
}
|
|
@@ -95,7 +95,7 @@ function getTransport() {
|
|
|
95
95
|
*/
|
|
96
96
|
function getWorkerEnabled(isDevelopment) {
|
|
97
97
|
// Explicit worker mode setting
|
|
98
|
-
const workerEnv = process.env.
|
|
98
|
+
const workerEnv = process.env.BLOOM_QUEUE_WORKER;
|
|
99
99
|
if (workerEnv !== undefined) {
|
|
100
100
|
return workerEnv.toLowerCase() === 'true';
|
|
101
101
|
}
|
|
@@ -116,24 +116,24 @@ function getWorkerEnabled(isDevelopment) {
|
|
|
116
116
|
*/
|
|
117
117
|
export function validateEnvironment() {
|
|
118
118
|
// Validate concurrency
|
|
119
|
-
const concurrency = process.env.
|
|
119
|
+
const concurrency = process.env.BLOOM_QUEUE_CONCURRENCY;
|
|
120
120
|
if (concurrency && (isNaN(parseInt(concurrency)) || parseInt(concurrency) < 1 || parseInt(concurrency) > 100)) {
|
|
121
|
-
throw new Error(`Invalid
|
|
121
|
+
throw new Error(`Invalid BLOOM_QUEUE_CONCURRENCY: "${concurrency}". Must be number between 1 and 100`);
|
|
122
122
|
}
|
|
123
123
|
// Validate max attempts
|
|
124
|
-
const maxAttempts = process.env.
|
|
124
|
+
const maxAttempts = process.env.BLOOM_QUEUE_MAX_ATTEMPTS;
|
|
125
125
|
if (maxAttempts && (isNaN(parseInt(maxAttempts)) || parseInt(maxAttempts) < 1 || parseInt(maxAttempts) > 10)) {
|
|
126
|
-
throw new Error(`Invalid
|
|
126
|
+
throw new Error(`Invalid BLOOM_QUEUE_MAX_ATTEMPTS: "${maxAttempts}". Must be number between 1 and 10`);
|
|
127
127
|
}
|
|
128
128
|
// Validate retry backoff
|
|
129
|
-
const backoff = process.env.
|
|
129
|
+
const backoff = process.env.BLOOM_QUEUE_RETRY_BACKOFF;
|
|
130
130
|
if (backoff && !['fixed', 'exponential'].includes(backoff)) {
|
|
131
|
-
throw new Error(`Invalid
|
|
131
|
+
throw new Error(`Invalid BLOOM_QUEUE_RETRY_BACKOFF: "${backoff}". Must be: fixed, exponential`);
|
|
132
132
|
}
|
|
133
133
|
// Validate transport selection
|
|
134
|
-
const transport = process.env.
|
|
134
|
+
const transport = process.env.BLOOM_QUEUE_TRANSPORT;
|
|
135
135
|
if (transport && !['memory', 'redis', 'database'].includes(transport)) {
|
|
136
|
-
throw new Error(`Invalid
|
|
136
|
+
throw new Error(`Invalid BLOOM_QUEUE_TRANSPORT: "${transport}". Must be: memory, redis, database`);
|
|
137
137
|
}
|
|
138
138
|
// Validate Redis URL if provided
|
|
139
139
|
const redisUrl = process.env.REDIS_URL;
|
|
@@ -146,15 +146,15 @@ export function validateEnvironment() {
|
|
|
146
146
|
throw new Error(`Invalid DATABASE_URL: "${dbUrl}". Must be valid database connection string`);
|
|
147
147
|
}
|
|
148
148
|
// Validate worker setting
|
|
149
|
-
const worker = process.env.
|
|
149
|
+
const worker = process.env.BLOOM_QUEUE_WORKER;
|
|
150
150
|
if (worker && !['true', 'false'].includes(worker.toLowerCase())) {
|
|
151
|
-
throw new Error(`Invalid
|
|
151
|
+
throw new Error(`Invalid BLOOM_QUEUE_WORKER: "${worker}". Must be: true, false`);
|
|
152
152
|
}
|
|
153
153
|
// Validate numeric values
|
|
154
|
-
validateNumericEnv('
|
|
155
|
-
validateNumericEnv('
|
|
156
|
-
validateNumericEnv('
|
|
157
|
-
validateNumericEnv('
|
|
154
|
+
validateNumericEnv('BLOOM_QUEUE_RETRY_DELAY', 1000, 300000); // 1s to 5min
|
|
155
|
+
validateNumericEnv('BLOOM_QUEUE_MEMORY_MAX_JOBS', 100, 100000); // 100 to 100k
|
|
156
|
+
validateNumericEnv('BLOOM_QUEUE_DB_POLL', 1000, 60000); // 1s to 1min
|
|
157
|
+
validateNumericEnv('BLOOM_QUEUE_SHUTDOWN_TIMEOUT', 5000, 120000); // 5s to 2min
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
160
|
* Validate numeric environment variable
|
|
@@ -48,7 +48,7 @@ export interface SecurityError extends Error {
|
|
|
48
48
|
[key: string]: any;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Gets smart defaults using
|
|
51
|
+
* Gets smart defaults using BLOOM_SECURITY_* environment variables
|
|
52
52
|
* @llm-rule WHEN: App startup to get production-ready security configuration
|
|
53
53
|
* @llm-rule AVOID: Calling repeatedly - expensive validation, cache the result
|
|
54
54
|
* @llm-rule NOTE: Automatically configures CSRF, rate limiting, and encryption from environment
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @llm-rule NOTE: Called once at startup, cached globally for performance
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* Gets smart defaults using
|
|
11
|
+
* Gets smart defaults using BLOOM_SECURITY_* environment variables
|
|
12
12
|
* @llm-rule WHEN: App startup to get production-ready security configuration
|
|
13
13
|
* @llm-rule AVOID: Calling repeatedly - expensive validation, cache the result
|
|
14
14
|
* @llm-rule NOTE: Automatically configures CSRF, rate limiting, and encryption from environment
|
|
@@ -22,28 +22,28 @@ export function getSmartDefaults() {
|
|
|
22
22
|
return {
|
|
23
23
|
// CSRF configuration with fallback to auth secret
|
|
24
24
|
csrf: {
|
|
25
|
-
secret: process.env.
|
|
26
|
-
tokenField: process.env.
|
|
27
|
-
headerField: process.env.
|
|
28
|
-
expiryMinutes: parseInt(process.env.
|
|
25
|
+
secret: process.env.BLOOM_SECURITY_CSRF_SECRET || process.env.BLOOM_AUTH_SECRET || '',
|
|
26
|
+
tokenField: process.env.BLOOM_SECURITY_CSRF_FIELD || '_csrf',
|
|
27
|
+
headerField: process.env.BLOOM_SECURITY_CSRF_HEADER || 'x-csrf-token',
|
|
28
|
+
expiryMinutes: parseInt(process.env.BLOOM_SECURITY_CSRF_EXPIRY || '60'),
|
|
29
29
|
},
|
|
30
30
|
// Rate limiting with production-ready defaults
|
|
31
31
|
rateLimit: {
|
|
32
|
-
maxRequests: parseInt(process.env.
|
|
33
|
-
windowMs: parseInt(process.env.
|
|
34
|
-
message: process.env.
|
|
32
|
+
maxRequests: parseInt(process.env.BLOOM_SECURITY_RATE_LIMIT || '100'),
|
|
33
|
+
windowMs: parseInt(process.env.BLOOM_SECURITY_RATE_WINDOW || String(15 * 60 * 1000)), // 15 minutes
|
|
34
|
+
message: process.env.BLOOM_SECURITY_RATE_MESSAGE || 'Too many requests, please try again later',
|
|
35
35
|
},
|
|
36
36
|
// Input sanitization configuration
|
|
37
37
|
sanitization: {
|
|
38
|
-
maxLength: parseInt(process.env.
|
|
39
|
-
allowedTags: process.env.
|
|
40
|
-
? process.env.
|
|
38
|
+
maxLength: parseInt(process.env.BLOOM_SECURITY_MAX_INPUT_LENGTH || '1000'),
|
|
39
|
+
allowedTags: process.env.BLOOM_SECURITY_ALLOWED_TAGS
|
|
40
|
+
? process.env.BLOOM_SECURITY_ALLOWED_TAGS.split(',').map(tag => tag.trim())
|
|
41
41
|
: [],
|
|
42
|
-
stripAllTags: process.env.
|
|
42
|
+
stripAllTags: process.env.BLOOM_SECURITY_STRIP_ALL_TAGS === 'true',
|
|
43
43
|
},
|
|
44
44
|
// Encryption configuration with AES-256-GCM
|
|
45
45
|
encryption: {
|
|
46
|
-
key: process.env.
|
|
46
|
+
key: process.env.BLOOM_SECURITY_ENCRYPTION_KEY,
|
|
47
47
|
algorithm: 'aes-256-gcm',
|
|
48
48
|
ivLength: 16,
|
|
49
49
|
tagLength: 16,
|
|
@@ -67,52 +67,52 @@ export function getSmartDefaults() {
|
|
|
67
67
|
function validateEnvironment() {
|
|
68
68
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
69
69
|
// Validate CSRF secret in production
|
|
70
|
-
const csrfSecret = process.env.
|
|
70
|
+
const csrfSecret = process.env.BLOOM_SECURITY_CSRF_SECRET || process.env.BLOOM_AUTH_SECRET;
|
|
71
71
|
if (!csrfSecret && nodeEnv === 'production') {
|
|
72
|
-
console.warn('[Bloomneo AppKit]
|
|
72
|
+
console.warn('[Bloomneo AppKit] BLOOM_SECURITY_CSRF_SECRET not set. ' +
|
|
73
73
|
'CSRF protection will not work in production. ' +
|
|
74
|
-
'Set
|
|
74
|
+
'Set BLOOM_SECURITY_CSRF_SECRET or BLOOM_AUTH_SECRET environment variable.');
|
|
75
75
|
}
|
|
76
76
|
// Validate encryption key if provided
|
|
77
|
-
const encryptionKey = process.env.
|
|
77
|
+
const encryptionKey = process.env.BLOOM_SECURITY_ENCRYPTION_KEY;
|
|
78
78
|
if (encryptionKey) {
|
|
79
79
|
validateEncryptionKey(encryptionKey);
|
|
80
80
|
}
|
|
81
81
|
// Validate rate limit values
|
|
82
|
-
const rateLimit = process.env.
|
|
82
|
+
const rateLimit = process.env.BLOOM_SECURITY_RATE_LIMIT;
|
|
83
83
|
if (rateLimit) {
|
|
84
84
|
const rateLimitNum = parseInt(rateLimit);
|
|
85
85
|
if (isNaN(rateLimitNum) || rateLimitNum <= 0) {
|
|
86
|
-
throw new Error(`Invalid
|
|
86
|
+
throw new Error(`Invalid BLOOM_SECURITY_RATE_LIMIT: "${rateLimit}". Must be a positive number.`);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
const rateWindow = process.env.
|
|
89
|
+
const rateWindow = process.env.BLOOM_SECURITY_RATE_WINDOW;
|
|
90
90
|
if (rateWindow) {
|
|
91
91
|
const rateWindowNum = parseInt(rateWindow);
|
|
92
92
|
if (isNaN(rateWindowNum) || rateWindowNum <= 0) {
|
|
93
|
-
throw new Error(`Invalid
|
|
93
|
+
throw new Error(`Invalid BLOOM_SECURITY_RATE_WINDOW: "${rateWindow}". Must be a positive number (milliseconds).`);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
// Validate max input length
|
|
97
|
-
const maxLength = process.env.
|
|
97
|
+
const maxLength = process.env.BLOOM_SECURITY_MAX_INPUT_LENGTH;
|
|
98
98
|
if (maxLength) {
|
|
99
99
|
const maxLengthNum = parseInt(maxLength);
|
|
100
100
|
if (isNaN(maxLengthNum) || maxLengthNum <= 0) {
|
|
101
|
-
throw new Error(`Invalid
|
|
101
|
+
throw new Error(`Invalid BLOOM_SECURITY_MAX_INPUT_LENGTH: "${maxLength}". Must be a positive number.`);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
// Validate CSRF expiry
|
|
105
|
-
const csrfExpiry = process.env.
|
|
105
|
+
const csrfExpiry = process.env.BLOOM_SECURITY_CSRF_EXPIRY;
|
|
106
106
|
if (csrfExpiry) {
|
|
107
107
|
const csrfExpiryNum = parseInt(csrfExpiry);
|
|
108
108
|
if (isNaN(csrfExpiryNum) || csrfExpiryNum <= 0) {
|
|
109
|
-
throw new Error(`Invalid
|
|
109
|
+
throw new Error(`Invalid BLOOM_SECURITY_CSRF_EXPIRY: "${csrfExpiry}". Must be a positive number (minutes).`);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
// Production-specific warnings
|
|
113
113
|
if (nodeEnv === 'production') {
|
|
114
114
|
if (!encryptionKey) {
|
|
115
|
-
console.warn('[Bloomneo AppKit]
|
|
115
|
+
console.warn('[Bloomneo AppKit] BLOOM_SECURITY_ENCRYPTION_KEY not set. ' +
|
|
116
116
|
'Data encryption will not be available in production.');
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -130,16 +130,16 @@ function validateEnvironment() {
|
|
|
130
130
|
*/
|
|
131
131
|
function validateEncryptionKey(key) {
|
|
132
132
|
if (typeof key !== 'string') {
|
|
133
|
-
throw new Error('
|
|
133
|
+
throw new Error('BLOOM_SECURITY_ENCRYPTION_KEY must be a string.');
|
|
134
134
|
}
|
|
135
135
|
// Check if it's a valid hex string
|
|
136
136
|
if (!/^[0-9a-fA-F]+$/.test(key)) {
|
|
137
|
-
throw new Error('
|
|
137
|
+
throw new Error('BLOOM_SECURITY_ENCRYPTION_KEY must be a valid hexadecimal string. ' +
|
|
138
138
|
'Generate one using: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
|
|
139
139
|
}
|
|
140
140
|
// Check length (should be 64 hex characters for 32 bytes)
|
|
141
141
|
if (key.length !== 64) {
|
|
142
|
-
throw new Error(`
|
|
142
|
+
throw new Error(`BLOOM_SECURITY_ENCRYPTION_KEY must be 64 hex characters (32 bytes). ` +
|
|
143
143
|
`Current length: ${key.length}. ` +
|
|
144
144
|
`Generate one using: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`);
|
|
145
145
|
}
|
package/dist/security/index.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ declare function isProduction(): boolean;
|
|
|
52
52
|
* Generate a secure encryption key for production use
|
|
53
53
|
* @llm-rule WHEN: Setting up encryption for the first time or rotating keys
|
|
54
54
|
* @llm-rule AVOID: Using weak or predictable keys - always use this for key generation
|
|
55
|
-
* @llm-rule NOTE: Returns 64-character hex string suitable for
|
|
55
|
+
* @llm-rule NOTE: Returns 64-character hex string suitable for BLOOM_SECURITY_ENCRYPTION_KEY
|
|
56
56
|
*/
|
|
57
57
|
declare function generateKey(): string;
|
|
58
58
|
/**
|
package/dist/security/index.js
CHANGED
|
@@ -78,7 +78,7 @@ function isProduction() {
|
|
|
78
78
|
* Generate a secure encryption key for production use
|
|
79
79
|
* @llm-rule WHEN: Setting up encryption for the first time or rotating keys
|
|
80
80
|
* @llm-rule AVOID: Using weak or predictable keys - always use this for key generation
|
|
81
|
-
* @llm-rule NOTE: Returns 64-character hex string suitable for
|
|
81
|
+
* @llm-rule NOTE: Returns 64-character hex string suitable for BLOOM_SECURITY_ENCRYPTION_KEY
|
|
82
82
|
*/
|
|
83
83
|
function generateKey() {
|
|
84
84
|
const security = get();
|
|
@@ -113,10 +113,10 @@ function validateRequired(checks = {}) {
|
|
|
113
113
|
const config = getConfig();
|
|
114
114
|
const missing = [];
|
|
115
115
|
if (checks.csrf && !config.csrf.secret) {
|
|
116
|
-
missing.push('
|
|
116
|
+
missing.push('BLOOM_SECURITY_CSRF_SECRET or BLOOM_AUTH_SECRET');
|
|
117
117
|
}
|
|
118
118
|
if (checks.encryption && !config.encryption.key) {
|
|
119
|
-
missing.push('
|
|
119
|
+
missing.push('BLOOM_SECURITY_ENCRYPTION_KEY');
|
|
120
120
|
}
|
|
121
121
|
if (missing.length > 0) {
|
|
122
122
|
throw new Error(`Missing required security configuration: ${missing.join(', ')}\n` +
|
|
@@ -111,7 +111,7 @@ export declare class SecurityClass {
|
|
|
111
111
|
* Generates a cryptographically secure 256-bit encryption key
|
|
112
112
|
* @llm-rule WHEN: Setting up encryption for the first time or rotating keys
|
|
113
113
|
* @llm-rule AVOID: Using weak or predictable keys - always use this method for key generation
|
|
114
|
-
* @llm-rule NOTE: Returns 64-character hex string suitable for
|
|
114
|
+
* @llm-rule NOTE: Returns 64-character hex string suitable for BLOOM_SECURITY_ENCRYPTION_KEY
|
|
115
115
|
*/
|
|
116
116
|
generateKey(): string;
|
|
117
117
|
/**
|
|
@@ -30,7 +30,7 @@ export class SecurityClass {
|
|
|
30
30
|
forms(options = {}) {
|
|
31
31
|
const csrfSecret = options.secret || this.config.csrf.secret;
|
|
32
32
|
if (!csrfSecret) {
|
|
33
|
-
throw createSecurityError('CSRF secret required. Set
|
|
33
|
+
throw createSecurityError('CSRF secret required. Set BLOOM_SECURITY_CSRF_SECRET or BLOOM_AUTH_SECRET environment variable', 500);
|
|
34
34
|
}
|
|
35
35
|
const tokenField = options.tokenField || this.config.csrf.tokenField;
|
|
36
36
|
const headerField = options.headerField || this.config.csrf.headerField;
|
|
@@ -242,7 +242,7 @@ export class SecurityClass {
|
|
|
242
242
|
}
|
|
243
243
|
const encryptionKey = key || this.config.encryption.key;
|
|
244
244
|
if (!encryptionKey) {
|
|
245
|
-
throw createSecurityError('Encryption key required. Provide as argument or set
|
|
245
|
+
throw createSecurityError('Encryption key required. Provide as argument or set BLOOM_SECURITY_ENCRYPTION_KEY environment variable', 500);
|
|
246
246
|
}
|
|
247
247
|
this.validateEncryptionKey(encryptionKey);
|
|
248
248
|
const keyBuffer = typeof encryptionKey === 'string'
|
|
@@ -284,7 +284,7 @@ export class SecurityClass {
|
|
|
284
284
|
}
|
|
285
285
|
const decryptionKey = key || this.config.encryption.key;
|
|
286
286
|
if (!decryptionKey) {
|
|
287
|
-
throw createSecurityError('Decryption key required. Provide as argument or set
|
|
287
|
+
throw createSecurityError('Decryption key required. Provide as argument or set BLOOM_SECURITY_ENCRYPTION_KEY environment variable', 500);
|
|
288
288
|
}
|
|
289
289
|
this.validateEncryptionKey(decryptionKey);
|
|
290
290
|
const keyBuffer = typeof decryptionKey === 'string'
|
|
@@ -331,7 +331,7 @@ export class SecurityClass {
|
|
|
331
331
|
* Generates a cryptographically secure 256-bit encryption key
|
|
332
332
|
* @llm-rule WHEN: Setting up encryption for the first time or rotating keys
|
|
333
333
|
* @llm-rule AVOID: Using weak or predictable keys - always use this method for key generation
|
|
334
|
-
* @llm-rule NOTE: Returns 64-character hex string suitable for
|
|
334
|
+
* @llm-rule NOTE: Returns 64-character hex string suitable for BLOOM_SECURITY_ENCRYPTION_KEY
|
|
335
335
|
*/
|
|
336
336
|
generateKey() {
|
|
337
337
|
try {
|
package/dist/storage/defaults.js
CHANGED
|
@@ -27,11 +27,11 @@ export function getSmartDefaults() {
|
|
|
27
27
|
strategy,
|
|
28
28
|
// Local configuration (only used when strategy is 'local')
|
|
29
29
|
local: {
|
|
30
|
-
dir: process.env.
|
|
31
|
-
baseUrl: process.env.
|
|
32
|
-
maxFileSize: parseInt(process.env.
|
|
30
|
+
dir: process.env.BLOOM_STORAGE_DIR || './uploads',
|
|
31
|
+
baseUrl: process.env.BLOOM_STORAGE_BASE_URL || '/uploads',
|
|
32
|
+
maxFileSize: parseInt(process.env.BLOOM_STORAGE_MAX_SIZE || '52428800'), // 50MB default
|
|
33
33
|
allowedTypes: parseAllowedTypes(),
|
|
34
|
-
createDirs: process.env.
|
|
34
|
+
createDirs: process.env.BLOOM_STORAGE_CREATE_DIRS !== 'false',
|
|
35
35
|
},
|
|
36
36
|
// S3 configuration (only used when strategy is 's3')
|
|
37
37
|
s3: {
|
|
@@ -41,8 +41,8 @@ export function getSmartDefaults() {
|
|
|
41
41
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID || process.env.S3_ACCESS_KEY_ID || '',
|
|
42
42
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || process.env.S3_SECRET_ACCESS_KEY || '',
|
|
43
43
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
|
44
|
-
signedUrlExpiry: parseInt(process.env.
|
|
45
|
-
cdnUrl: process.env.
|
|
44
|
+
signedUrlExpiry: parseInt(process.env.BLOOM_STORAGE_SIGNED_EXPIRY || '3600'), // 1 hour
|
|
45
|
+
cdnUrl: process.env.BLOOM_STORAGE_CDN_URL,
|
|
46
46
|
},
|
|
47
47
|
// R2 configuration (only used when strategy is 'r2')
|
|
48
48
|
r2: {
|
|
@@ -51,7 +51,7 @@ export function getSmartDefaults() {
|
|
|
51
51
|
accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID || '',
|
|
52
52
|
secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY || '',
|
|
53
53
|
cdnUrl: process.env.CLOUDFLARE_R2_CDN_URL,
|
|
54
|
-
signedUrlExpiry: parseInt(process.env.
|
|
54
|
+
signedUrlExpiry: parseInt(process.env.BLOOM_STORAGE_SIGNED_EXPIRY || '3600'), // 1 hour
|
|
55
55
|
},
|
|
56
56
|
// Environment information
|
|
57
57
|
environment: {
|
|
@@ -70,7 +70,7 @@ export function getSmartDefaults() {
|
|
|
70
70
|
*/
|
|
71
71
|
function detectStorageStrategy() {
|
|
72
72
|
// Explicit override wins (for testing/debugging)
|
|
73
|
-
const explicit = process.env.
|
|
73
|
+
const explicit = process.env.BLOOM_STORAGE_STRATEGY?.toLowerCase();
|
|
74
74
|
if (explicit && ['local', 's3', 'r2'].includes(explicit)) {
|
|
75
75
|
return explicit;
|
|
76
76
|
}
|
|
@@ -95,7 +95,7 @@ function detectStorageStrategy() {
|
|
|
95
95
|
* @llm-rule AVOID: Allowing all file types in production - security risk
|
|
96
96
|
*/
|
|
97
97
|
function parseAllowedTypes() {
|
|
98
|
-
const envTypes = process.env.
|
|
98
|
+
const envTypes = process.env.BLOOM_STORAGE_ALLOWED_TYPES;
|
|
99
99
|
if (!envTypes) {
|
|
100
100
|
// Safe defaults - common web file types
|
|
101
101
|
return [
|
|
@@ -108,7 +108,7 @@ function parseAllowedTypes() {
|
|
|
108
108
|
if (envTypes === '*') {
|
|
109
109
|
if (process.env.NODE_ENV === 'production') {
|
|
110
110
|
console.warn('[Bloomneo AppKit] SECURITY WARNING: All file types allowed in production. ' +
|
|
111
|
-
'Set
|
|
111
|
+
'Set BLOOM_STORAGE_ALLOWED_TYPES to specific types for security.');
|
|
112
112
|
}
|
|
113
113
|
return ['*']; // Allow all types (use with caution)
|
|
114
114
|
}
|
|
@@ -122,13 +122,13 @@ function parseAllowedTypes() {
|
|
|
122
122
|
*/
|
|
123
123
|
function validateEnvironment() {
|
|
124
124
|
// Validate storage strategy if explicitly set
|
|
125
|
-
const strategy = process.env.
|
|
125
|
+
const strategy = process.env.BLOOM_STORAGE_STRATEGY;
|
|
126
126
|
if (strategy && !['local', 's3', 'r2'].includes(strategy.toLowerCase())) {
|
|
127
|
-
throw new Error(`Invalid
|
|
127
|
+
throw new Error(`Invalid BLOOM_STORAGE_STRATEGY: "${strategy}". Must be "local", "s3", or "r2"`);
|
|
128
128
|
}
|
|
129
129
|
// Validate numeric values
|
|
130
|
-
validateNumericEnv('
|
|
131
|
-
validateNumericEnv('
|
|
130
|
+
validateNumericEnv('BLOOM_STORAGE_MAX_SIZE', 1048576, 1073741824); // 1MB to 1GB
|
|
131
|
+
validateNumericEnv('BLOOM_STORAGE_SIGNED_EXPIRY', 60, 604800); // 1 minute to 7 days
|
|
132
132
|
// Validate S3 configuration if S3 strategy detected
|
|
133
133
|
if (shouldValidateS3()) {
|
|
134
134
|
validateS3Config();
|
|
@@ -214,14 +214,14 @@ function validateR2Config() {
|
|
|
214
214
|
* Validates local configuration
|
|
215
215
|
*/
|
|
216
216
|
function validateLocalConfig() {
|
|
217
|
-
const dir = process.env.
|
|
217
|
+
const dir = process.env.BLOOM_STORAGE_DIR;
|
|
218
218
|
if (dir && (dir.includes('..') || dir.startsWith('/') && process.env.NODE_ENV === 'production')) {
|
|
219
219
|
console.warn(`[Bloomneo AppKit] Potentially unsafe storage directory: "${dir}". ` +
|
|
220
220
|
`Consider using a relative path for security.`);
|
|
221
221
|
}
|
|
222
|
-
const baseUrl = process.env.
|
|
222
|
+
const baseUrl = process.env.BLOOM_STORAGE_BASE_URL;
|
|
223
223
|
if (baseUrl && !baseUrl.startsWith('/') && !isValidUrl(baseUrl)) {
|
|
224
|
-
throw new Error(`Invalid
|
|
224
|
+
throw new Error(`Invalid BLOOM_STORAGE_BASE_URL: "${baseUrl}". Must be a path or valid URL`);
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
@@ -237,10 +237,10 @@ function validateProductionConfig() {
|
|
|
237
237
|
'Set AWS_S3_BUCKET or CLOUDFLARE_R2_BUCKET for distributed storage.');
|
|
238
238
|
}
|
|
239
239
|
// Warn about missing CDN in production
|
|
240
|
-
const cdnUrl = process.env.
|
|
240
|
+
const cdnUrl = process.env.BLOOM_STORAGE_CDN_URL || process.env.CLOUDFLARE_R2_CDN_URL;
|
|
241
241
|
if (!cdnUrl && strategy !== 'local') {
|
|
242
242
|
console.warn('[Bloomneo AppKit] No CDN URL configured in production. ' +
|
|
243
|
-
'Set
|
|
243
|
+
'Set BLOOM_STORAGE_CDN_URL for better performance.');
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
/**
|
package/dist/util/defaults.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface UtilConfig {
|
|
|
51
51
|
environment: EnvironmentConfig;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* Gets smart defaults using
|
|
54
|
+
* Gets smart defaults using BLOOM_UTIL_* environment variables
|
|
55
55
|
* @llm-rule WHEN: App startup to get production-ready utility configuration
|
|
56
56
|
* @llm-rule AVOID: Calling repeatedly - expensive validation, cache the result
|
|
57
57
|
* @llm-rule NOTE: Called once at startup, cached globally for performance
|