@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +2 -1844
  2. package/README.md +44 -18
  3. package/dist/cli/commands/add.js +325 -0
  4. package/dist/config/service-schema-config.js +98 -5
  5. package/dist/index.js +22 -3
  6. package/dist/middleware/Composer.js +2 -1
  7. package/dist/middleware/factories.js +445 -0
  8. package/dist/middleware/index.js +4 -1
  9. package/dist/modules/ModuleManager.js +6 -2
  10. package/dist/routing/EnhancedRouter.js +248 -44
  11. package/dist/routing/RequestContext.js +393 -0
  12. package/dist/schema/SchemaManager.js +6 -2
  13. package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +79 -223
  14. package/dist/service-management/generators/code/WorkerIndexGenerator.js +241 -98
  15. package/dist/service-management/generators/config/WranglerTomlGenerator.js +130 -89
  16. package/dist/simple-api.js +4 -4
  17. package/dist/utilities/index.js +134 -1
  18. package/dist/utils/config/environment-var-normalizer.js +233 -0
  19. package/dist/validation/environmentGuard.js +172 -0
  20. package/docs/CHANGELOG.md +1877 -0
  21. package/docs/api-reference.md +153 -0
  22. package/package.json +4 -1
  23. package/scripts/repro-clodo.js +123 -0
  24. package/templates/ai-worker/package.json +19 -0
  25. package/templates/ai-worker/src/index.js +160 -0
  26. package/templates/cron-worker/package.json +19 -0
  27. package/templates/cron-worker/src/index.js +211 -0
  28. package/templates/edge-proxy/package.json +18 -0
  29. package/templates/edge-proxy/src/index.js +150 -0
  30. package/templates/minimal/package.json +17 -0
  31. package/templates/minimal/src/index.js +40 -0
  32. package/templates/queue-processor/package.json +19 -0
  33. package/templates/queue-processor/src/index.js +213 -0
  34. package/templates/rest-api/.dev.vars +2 -0
  35. package/templates/rest-api/package.json +19 -0
  36. package/templates/rest-api/src/index.js +124 -0
@@ -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
+ }