@tamyla/clodo-framework 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -1844
- package/README.md +44 -18
- package/dist/cli/commands/add.js +325 -0
- package/dist/config/service-schema-config.js +98 -5
- package/dist/index.js +22 -3
- package/dist/middleware/Composer.js +2 -1
- package/dist/middleware/factories.js +445 -0
- package/dist/middleware/index.js +4 -1
- package/dist/modules/ModuleManager.js +6 -2
- package/dist/routing/EnhancedRouter.js +248 -44
- package/dist/routing/RequestContext.js +393 -0
- package/dist/schema/SchemaManager.js +6 -2
- package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +79 -223
- package/dist/service-management/generators/code/WorkerIndexGenerator.js +241 -98
- package/dist/service-management/generators/config/WranglerTomlGenerator.js +130 -89
- package/dist/simple-api.js +4 -4
- package/dist/utilities/index.js +134 -1
- package/dist/utils/config/environment-var-normalizer.js +233 -0
- package/dist/validation/environmentGuard.js +172 -0
- package/docs/CHANGELOG.md +1877 -0
- package/docs/api-reference.md +153 -0
- package/package.json +4 -1
- package/scripts/repro-clodo.js +123 -0
- package/templates/ai-worker/package.json +19 -0
- package/templates/ai-worker/src/index.js +160 -0
- package/templates/cron-worker/package.json +19 -0
- package/templates/cron-worker/src/index.js +211 -0
- package/templates/edge-proxy/package.json +18 -0
- package/templates/edge-proxy/src/index.js +150 -0
- package/templates/minimal/package.json +17 -0
- package/templates/minimal/src/index.js +40 -0
- package/templates/queue-processor/package.json +19 -0
- package/templates/queue-processor/src/index.js +213 -0
- package/templates/rest-api/.dev.vars +2 -0
- package/templates/rest-api/package.json +19 -0
- package/templates/rest-api/src/index.js +124 -0
package/dist/utilities/index.js
CHANGED
|
@@ -62,4 +62,137 @@ export { ServiceBindingClient, RPCClient, ServiceRouter } from './bindings/index
|
|
|
62
62
|
// ============================================================
|
|
63
63
|
|
|
64
64
|
// Analytics Engine
|
|
65
|
-
export { AnalyticsWriter, EventTracker, MetricsCollector } from './analytics/index.js';
|
|
65
|
+
export { AnalyticsWriter, EventTracker, MetricsCollector } from './analytics/index.js';
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// STANDALONE UTILITY FUNCTIONS
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
// AI Utilities
|
|
72
|
+
/**
|
|
73
|
+
* Run an AI model directly
|
|
74
|
+
* @param {Object} aiBinding - AI binding from env
|
|
75
|
+
* @param {string} model - Model name
|
|
76
|
+
* @param {Object} inputs - Model inputs
|
|
77
|
+
* @returns {Promise<Object>} Model response
|
|
78
|
+
*/
|
|
79
|
+
export async function runAIModel(aiBinding, model, inputs) {
|
|
80
|
+
const {
|
|
81
|
+
AIClient
|
|
82
|
+
} = await import('./ai/index.js');
|
|
83
|
+
const ai = new AIClient(aiBinding);
|
|
84
|
+
return ai.run(model, inputs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Stream AI response
|
|
89
|
+
* @param {Object} aiBinding - AI binding from env
|
|
90
|
+
* @param {string} model - Model name
|
|
91
|
+
* @param {Object} inputs - Model inputs
|
|
92
|
+
* @returns {Promise<Response>} Streaming response
|
|
93
|
+
*/
|
|
94
|
+
export async function streamAIResponse(aiBinding, model, inputs) {
|
|
95
|
+
const {
|
|
96
|
+
AIClient,
|
|
97
|
+
streamResponse
|
|
98
|
+
} = await import('./ai/index.js');
|
|
99
|
+
const ai = new AIClient(aiBinding);
|
|
100
|
+
const stream = await ai.run(model, {
|
|
101
|
+
...inputs,
|
|
102
|
+
stream: true
|
|
103
|
+
});
|
|
104
|
+
return streamResponse(stream);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format AI prompt with context
|
|
109
|
+
* @param {string} prompt - Base prompt
|
|
110
|
+
* @param {Object} context - Context data
|
|
111
|
+
* @returns {string} Formatted prompt
|
|
112
|
+
*/
|
|
113
|
+
export function formatAIPrompt(prompt, context = {}) {
|
|
114
|
+
let formatted = prompt;
|
|
115
|
+
|
|
116
|
+
// Replace placeholders like {{key}} with context values
|
|
117
|
+
for (const [key, value] of Object.entries(context)) {
|
|
118
|
+
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
|
119
|
+
formatted = formatted.replace(placeholder, String(value));
|
|
120
|
+
}
|
|
121
|
+
return formatted;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Vectorize Utilities
|
|
125
|
+
/**
|
|
126
|
+
* Query vectors from Vectorize index
|
|
127
|
+
* @param {Object} vectorizeBinding - Vectorize binding from env
|
|
128
|
+
* @param {number[]} vector - Query vector
|
|
129
|
+
* @param {Object} options - Query options
|
|
130
|
+
* @returns {Promise<Object>} Query results
|
|
131
|
+
*/
|
|
132
|
+
export async function queryVectors(vectorizeBinding, vector, options = {}) {
|
|
133
|
+
const {
|
|
134
|
+
VectorStore
|
|
135
|
+
} = await import('./vectorize/index.js');
|
|
136
|
+
const store = new VectorStore(vectorizeBinding);
|
|
137
|
+
return store.query(vector, options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Upsert vectors to Vectorize index
|
|
142
|
+
* @param {Object} vectorizeBinding - Vectorize binding from env
|
|
143
|
+
* @param {Array} vectors - Vectors to upsert
|
|
144
|
+
* @returns {Promise<Object>} Upsert results
|
|
145
|
+
*/
|
|
146
|
+
export async function upsertVectors(vectorizeBinding, vectors) {
|
|
147
|
+
const {
|
|
148
|
+
VectorStore
|
|
149
|
+
} = await import('./vectorize/index.js');
|
|
150
|
+
const store = new VectorStore(vectorizeBinding);
|
|
151
|
+
return store.upsert(vectors);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// KV Utilities
|
|
155
|
+
/**
|
|
156
|
+
* Get value from KV storage
|
|
157
|
+
* @param {Object} kvBinding - KV binding from env
|
|
158
|
+
* @param {string} key - Key to retrieve
|
|
159
|
+
* @param {Object} options - Get options
|
|
160
|
+
* @returns {Promise<*>} Retrieved value
|
|
161
|
+
*/
|
|
162
|
+
export async function getKV(kvBinding, key, options = {}) {
|
|
163
|
+
const {
|
|
164
|
+
KVStorage
|
|
165
|
+
} = await import('./kv/index.js');
|
|
166
|
+
const kv = new KVStorage(kvBinding);
|
|
167
|
+
return kv.get(key, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Put value in KV storage
|
|
172
|
+
* @param {Object} kvBinding - KV binding from env
|
|
173
|
+
* @param {string} key - Key to set
|
|
174
|
+
* @param {*} value - Value to store
|
|
175
|
+
* @param {Object} options - Put options
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
export async function putKV(kvBinding, key, value, options = {}) {
|
|
179
|
+
const {
|
|
180
|
+
KVStorage
|
|
181
|
+
} = await import('./kv/index.js');
|
|
182
|
+
const kv = new KVStorage(kvBinding);
|
|
183
|
+
return kv.set(key, value, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List keys from KV storage
|
|
188
|
+
* @param {Object} kvBinding - KV binding from env
|
|
189
|
+
* @param {Object} options - List options
|
|
190
|
+
* @returns {Promise<Object>} List results
|
|
191
|
+
*/
|
|
192
|
+
export async function listKV(kvBinding, options = {}) {
|
|
193
|
+
const {
|
|
194
|
+
KVStorage
|
|
195
|
+
} = await import('./kv/index.js');
|
|
196
|
+
const kv = new KVStorage(kvBinding);
|
|
197
|
+
return kv.list(options);
|
|
198
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment Variable Normalizer
|
|
5
|
+
* Standardizes service environment variable configuration across multiple formats
|
|
6
|
+
*
|
|
7
|
+
* Supports three legacy formats for backward compatibility:
|
|
8
|
+
* 1. Flat structure (recommended):
|
|
9
|
+
* service.vars = { API_KEY: "value" }
|
|
10
|
+
*
|
|
11
|
+
* 2. Nested structure (deprecated):
|
|
12
|
+
* service.environment = { vars: { API_KEY: "value" }, secrets: [...] }
|
|
13
|
+
*
|
|
14
|
+
* 3. Per-environment overrides (deprecated):
|
|
15
|
+
* service.env = { production: { vars: { ... } }, staging: { vars: { ... } } }
|
|
16
|
+
*
|
|
17
|
+
* @module EnvironmentVarNormalizer
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* EnvironmentVarNormalizer
|
|
22
|
+
* Handles conversion and validation of service environment variable formats
|
|
23
|
+
*/
|
|
24
|
+
export class EnvironmentVarNormalizer {
|
|
25
|
+
/**
|
|
26
|
+
* Normalize service configuration to standard flat structure
|
|
27
|
+
* Accepts all 3 formats and converts to unified output
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} service - Service configuration object
|
|
30
|
+
* @param {Object} options - Normalization options
|
|
31
|
+
* @param {boolean} options.warnOnDeprecated - Log deprecation warnings (default: true)
|
|
32
|
+
* @param {boolean} options.throwOnConflict - Throw error if conflicting formats found (default: false)
|
|
33
|
+
* @returns {Object} Normalized service config with flat vars and secrets
|
|
34
|
+
*/
|
|
35
|
+
static normalize(service, options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
warnOnDeprecated = true,
|
|
38
|
+
throwOnConflict = false
|
|
39
|
+
} = options;
|
|
40
|
+
const result = {
|
|
41
|
+
...service,
|
|
42
|
+
vars: {},
|
|
43
|
+
secrets: [],
|
|
44
|
+
_normalizationInfo: {
|
|
45
|
+
formatDetected: null,
|
|
46
|
+
deprecatedFormatsFound: [],
|
|
47
|
+
warnings: []
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Check which formats are present
|
|
52
|
+
const hasFlat = 'vars' in service;
|
|
53
|
+
const hasNested = 'environment' in service && service.environment?.vars;
|
|
54
|
+
const hasPerEnv = 'env' in service && this.hasPerEnvironmentVars(service.env);
|
|
55
|
+
|
|
56
|
+
// Track what we found
|
|
57
|
+
const formatsFound = [];
|
|
58
|
+
if (hasFlat) formatsFound.push('flat');
|
|
59
|
+
if (hasNested) formatsFound.push('nested');
|
|
60
|
+
if (hasPerEnv) formatsFound.push('per-environment');
|
|
61
|
+
|
|
62
|
+
// Handle conflicts
|
|
63
|
+
if (formatsFound.length > 1) {
|
|
64
|
+
const warning = `Service '${service.name}' uses multiple var formats: [${formatsFound.join(', ')}]. Using flat format as primary.`;
|
|
65
|
+
result._normalizationInfo.warnings.push(warning);
|
|
66
|
+
if (warnOnDeprecated) {
|
|
67
|
+
console.warn(`⚠️ ${warning}`);
|
|
68
|
+
}
|
|
69
|
+
if (throwOnConflict) {
|
|
70
|
+
throw new Error(`Configuration conflict: Service '${service.name}' has conflicting var formats. ` + `Please use only the flat format: service.vars = {...}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract flat format (primary)
|
|
75
|
+
if (hasFlat && service.vars && typeof service.vars === 'object') {
|
|
76
|
+
result.vars = {
|
|
77
|
+
...service.vars
|
|
78
|
+
};
|
|
79
|
+
result._normalizationInfo.formatDetected = 'flat';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extract flat secrets
|
|
83
|
+
if (Array.isArray(service.secrets)) {
|
|
84
|
+
result.secrets = [...service.secrets];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Extract nested format (deprecated)
|
|
88
|
+
if (hasNested) {
|
|
89
|
+
result._normalizationInfo.deprecatedFormatsFound.push('nested');
|
|
90
|
+
if (warnOnDeprecated) {
|
|
91
|
+
console.warn(`⚠️ DEPRECATION: Service '${service.name}' uses nested format (service.environment.vars). ` + `This will be removed in v5.0.0. Use flat format instead: service.vars = {...}`);
|
|
92
|
+
}
|
|
93
|
+
const nestedVars = service.environment.vars || {};
|
|
94
|
+
Object.assign(result.vars, nestedVars);
|
|
95
|
+
|
|
96
|
+
// Extract secrets from nested format (only if not already set from flat)
|
|
97
|
+
if (Array.isArray(service.environment.secrets) && result.secrets.length === 0) {
|
|
98
|
+
result.secrets = [...service.environment.secrets];
|
|
99
|
+
}
|
|
100
|
+
if (!result._normalizationInfo.formatDetected) {
|
|
101
|
+
result._normalizationInfo.formatDetected = 'nested';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract per-environment format (deprecated)
|
|
106
|
+
if (hasPerEnv) {
|
|
107
|
+
result._normalizationInfo.deprecatedFormatsFound.push('per-environment');
|
|
108
|
+
if (warnOnDeprecated) {
|
|
109
|
+
console.warn(`⚠️ DEPRECATION: Service '${service.name}' uses per-environment format (service.env.{environment}.vars). ` + `This will be removed in v5.0.0. Use flat format instead: service.vars = {...} ` + `(Clodo will handle environment-specific overrides internally)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Merge all environment vars
|
|
113
|
+
for (const [envName, envConfig] of Object.entries(service.env)) {
|
|
114
|
+
if (envConfig?.vars && typeof envConfig.vars === 'object') {
|
|
115
|
+
Object.assign(result.vars, envConfig.vars);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!result._normalizationInfo.formatDetected) {
|
|
119
|
+
result._normalizationInfo.formatDetected = 'per-environment';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Remove deprecated properties from result
|
|
124
|
+
if (hasNested) {
|
|
125
|
+
delete result.environment;
|
|
126
|
+
}
|
|
127
|
+
if (hasPerEnv) {
|
|
128
|
+
// Don't delete env - it might have other valid properties like name
|
|
129
|
+
// Just remove the vars from each environment config
|
|
130
|
+
if (result.env && typeof result.env === 'object') {
|
|
131
|
+
for (const envConfig of Object.values(result.env)) {
|
|
132
|
+
if (envConfig?.vars) {
|
|
133
|
+
delete envConfig.vars;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if env object contains per-environment var configs
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
static hasPerEnvironmentVars(env) {
|
|
146
|
+
if (!env || typeof env !== 'object') return false;
|
|
147
|
+
for (const envConfig of Object.values(env)) {
|
|
148
|
+
if (envConfig?.vars && typeof envConfig.vars === 'object') {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get deprecation timeline for the current version
|
|
157
|
+
* @param {string} currentVersion - Current framework version (e.g., "4.4.1")
|
|
158
|
+
* @returns {Object} Deprecation timeline with dates and versions
|
|
159
|
+
*/
|
|
160
|
+
static getDeprecationTimeline(currentVersion = '4.4.1') {
|
|
161
|
+
return {
|
|
162
|
+
current: {
|
|
163
|
+
version: currentVersion,
|
|
164
|
+
status: 'DEPRECATED (but supported)',
|
|
165
|
+
message: 'Nested and per-environment formats are still supported but generate warnings'
|
|
166
|
+
},
|
|
167
|
+
v4_5_0: {
|
|
168
|
+
version: '4.5.0',
|
|
169
|
+
eta: 'Q2 2026 (May 2026)',
|
|
170
|
+
status: 'WARNINGS REQUIRED',
|
|
171
|
+
message: 'All uses of deprecated formats must emit console warnings during deployment'
|
|
172
|
+
},
|
|
173
|
+
v5_0_0: {
|
|
174
|
+
version: '5.0.0',
|
|
175
|
+
eta: 'Q3 2026 (July 2026)',
|
|
176
|
+
status: 'REMOVAL',
|
|
177
|
+
message: 'Nested and per-environment formats will no longer be supported. Only flat format accepted.'
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate that vars follow naming conventions
|
|
184
|
+
* @param {Object} vars - Variables object
|
|
185
|
+
* @returns {Object} Validation result with issues array
|
|
186
|
+
*/
|
|
187
|
+
static validateNamingConventions(vars) {
|
|
188
|
+
const issues = [];
|
|
189
|
+
if (!vars || typeof vars !== 'object') {
|
|
190
|
+
return {
|
|
191
|
+
valid: true,
|
|
192
|
+
issues
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
196
|
+
// Check for hyphens first
|
|
197
|
+
if (key.includes('-')) {
|
|
198
|
+
issues.push({
|
|
199
|
+
key,
|
|
200
|
+
issue: 'Hyphens not allowed in variable names',
|
|
201
|
+
message: `Variable name '${key}' contains hyphens. Use underscores instead: ${key.replace(/-/g, '_')}`,
|
|
202
|
+
severity: 'error'
|
|
203
|
+
});
|
|
204
|
+
continue; // Skip other checks for this key
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check for dots
|
|
208
|
+
if (key.includes('.')) {
|
|
209
|
+
issues.push({
|
|
210
|
+
key,
|
|
211
|
+
issue: 'Dots not allowed in variable names',
|
|
212
|
+
message: `Variable name '${key}' contains dots. Use underscores instead: ${key.replace(/\./g, '_')}`,
|
|
213
|
+
severity: 'error'
|
|
214
|
+
});
|
|
215
|
+
continue; // Skip other checks for this key
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check for valid identifier format
|
|
219
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
|
|
220
|
+
issues.push({
|
|
221
|
+
key,
|
|
222
|
+
issue: 'Invalid variable name format',
|
|
223
|
+
message: `Variable name '${key}' doesn't follow SCREAMING_SNAKE_CASE convention. ` + `Use uppercase letters, numbers, and underscores only (must start with letter or underscore).`,
|
|
224
|
+
severity: 'warning'
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
valid: issues.filter(i => i.severity === 'error').length === 0,
|
|
230
|
+
issues
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Guard — Validates Cloudflare Worker env bindings at startup
|
|
3
|
+
*
|
|
4
|
+
* Catches missing bindings early with descriptive errors instead of
|
|
5
|
+
* failing deep in handler logic with cryptic "Cannot read property of undefined".
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { createEnvironmentGuard } from '@tamyla/clodo-framework';
|
|
9
|
+
*
|
|
10
|
+
* const guard = createEnvironmentGuard({
|
|
11
|
+
* required: ['KV_DATA', 'AI', 'SECRET_KEY'],
|
|
12
|
+
* optional: ['DEBUG', 'ANTHROPIC_API_KEY'],
|
|
13
|
+
* validate: {
|
|
14
|
+
* SECRET_KEY: (v) => typeof v === 'string' && v.length >= 32
|
|
15
|
+
* }
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // In your Worker fetch handler:
|
|
19
|
+
* export default {
|
|
20
|
+
* async fetch(request, env, ctx) {
|
|
21
|
+
* guard.check(env); // throws if bindings are missing
|
|
22
|
+
* // ... handle request
|
|
23
|
+
* }
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* @module @tamyla/clodo-framework/validation/environmentGuard
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} EnvironmentGuardConfig
|
|
31
|
+
* @property {string[]} required - Binding names that MUST be present
|
|
32
|
+
* @property {string[]} [optional=[]] - Binding names that MAY be present
|
|
33
|
+
* @property {Object<string, Function>} [validate={}] - Custom validators per binding
|
|
34
|
+
* @property {boolean} [throwOnMissing=true] - Throw on missing required binding (false = return report)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export class EnvironmentGuard {
|
|
38
|
+
/**
|
|
39
|
+
* @param {EnvironmentGuardConfig} config
|
|
40
|
+
*/
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
this.required = config.required || [];
|
|
43
|
+
this.optional = config.optional || [];
|
|
44
|
+
this.validators = config.validate || {};
|
|
45
|
+
this.throwOnMissing = config.throwOnMissing !== false;
|
|
46
|
+
this._checked = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate env bindings. Call once at startup or in the fetch handler.
|
|
51
|
+
* @param {Object} env - Cloudflare Worker environment
|
|
52
|
+
* @returns {{ valid: boolean, missing: string[], invalid: string[], present: string[], warnings: string[] }}
|
|
53
|
+
* @throws {Error} If throwOnMissing is true and required bindings are missing
|
|
54
|
+
*/
|
|
55
|
+
check(env) {
|
|
56
|
+
const result = {
|
|
57
|
+
valid: true,
|
|
58
|
+
missing: [],
|
|
59
|
+
invalid: [],
|
|
60
|
+
present: [],
|
|
61
|
+
warnings: []
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Check required bindings
|
|
65
|
+
for (const name of this.required) {
|
|
66
|
+
if (env[name] === undefined || env[name] === null) {
|
|
67
|
+
result.missing.push(name);
|
|
68
|
+
result.valid = false;
|
|
69
|
+
} else {
|
|
70
|
+
result.present.push(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check optional bindings (warn if missing, don't fail)
|
|
75
|
+
for (const name of this.optional) {
|
|
76
|
+
if (env[name] === undefined || env[name] === null) {
|
|
77
|
+
result.warnings.push(`Optional binding '${name}' is not configured`);
|
|
78
|
+
} else {
|
|
79
|
+
result.present.push(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Run custom validators
|
|
84
|
+
for (const [name, validator] of Object.entries(this.validators)) {
|
|
85
|
+
if (env[name] !== undefined && env[name] !== null) {
|
|
86
|
+
try {
|
|
87
|
+
const isValid = validator(env[name]);
|
|
88
|
+
if (!isValid) {
|
|
89
|
+
result.invalid.push(name);
|
|
90
|
+
result.valid = false;
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
result.invalid.push(name);
|
|
94
|
+
result.valid = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this._checked = true;
|
|
99
|
+
if (!result.valid && this.throwOnMissing) {
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (result.missing.length) {
|
|
102
|
+
parts.push(`Missing required bindings: ${result.missing.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
if (result.invalid.length) {
|
|
105
|
+
parts.push(`Invalid bindings: ${result.invalid.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`[EnvironmentGuard] Environment validation failed.\n${parts.join('\n')}\n\n` + `Hint: Check your wrangler.toml bindings and .dev.vars for secrets.`);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a middleware-compatible object that validates env on each request (with caching).
|
|
114
|
+
* Can be used with router.use() or composeMiddleware().
|
|
115
|
+
* @returns {Object} Middleware object with preprocess method
|
|
116
|
+
*/
|
|
117
|
+
asMiddleware() {
|
|
118
|
+
const guard = this;
|
|
119
|
+
return {
|
|
120
|
+
preprocess(request, env) {
|
|
121
|
+
if (!guard._checked) {
|
|
122
|
+
guard.check(env);
|
|
123
|
+
}
|
|
124
|
+
return null; // pass through
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a TypeScript Env interface from the configured bindings.
|
|
131
|
+
* Useful for DX — generates type definitions from guard config.
|
|
132
|
+
* @param {Object} [bindingTypes={}] - Map of binding name → TypeScript type
|
|
133
|
+
* @returns {string} TypeScript interface definition
|
|
134
|
+
*/
|
|
135
|
+
generateEnvType(bindingTypes = {}) {
|
|
136
|
+
const lines = ['interface Env {'];
|
|
137
|
+
|
|
138
|
+
// Known Cloudflare binding types
|
|
139
|
+
const defaultTypes = {
|
|
140
|
+
KVNamespace: 'KVNamespace',
|
|
141
|
+
D1Database: 'D1Database',
|
|
142
|
+
R2Bucket: 'R2Bucket',
|
|
143
|
+
Ai: 'Ai',
|
|
144
|
+
VectorizeIndex: 'VectorizeIndex',
|
|
145
|
+
Queue: 'Queue',
|
|
146
|
+
DurableObjectNamespace: 'DurableObjectNamespace',
|
|
147
|
+
Fetcher: 'Fetcher',
|
|
148
|
+
AnalyticsEngineDataset: 'AnalyticsEngineDataset',
|
|
149
|
+
SendEmail: 'SendEmail',
|
|
150
|
+
Hyperdrive: 'Hyperdrive'
|
|
151
|
+
};
|
|
152
|
+
for (const name of this.required) {
|
|
153
|
+
const type = bindingTypes[name] || 'unknown';
|
|
154
|
+
lines.push(` ${name}: ${type};`);
|
|
155
|
+
}
|
|
156
|
+
for (const name of this.optional) {
|
|
157
|
+
const type = bindingTypes[name] || 'string';
|
|
158
|
+
lines.push(` ${name}?: ${type};`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('}');
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create an environment guard — factory function
|
|
167
|
+
* @param {EnvironmentGuardConfig} config
|
|
168
|
+
* @returns {EnvironmentGuard}
|
|
169
|
+
*/
|
|
170
|
+
export function createEnvironmentGuard(config = {}) {
|
|
171
|
+
return new EnvironmentGuard(config);
|
|
172
|
+
}
|