@intranefr/superbackend 1.5.0 → 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.
Files changed (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +185 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +738 -0
  120. package/src/services/consoleOverride.service.js +7 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -1,6 +1,8 @@
1
1
  const ScriptDefinition = require('../models/ScriptDefinition');
2
2
  const ScriptRun = require('../models/ScriptRun');
3
+ const { basicAuth } = require('../middleware/auth');
3
4
  const { startRun, getRunBus } = require('../services/scriptsRunner.service');
5
+ const { logAuditSync } = require('../services/auditLogger');
4
6
 
5
7
  function toSafeJsonError(error) {
6
8
  const msg = error?.message || 'Operation failed';
@@ -23,6 +25,35 @@ function normalizeEnv(env) {
23
25
  return out;
24
26
  }
25
27
 
28
+ // Helper functions for base64 handling
29
+ function isBase64(str) {
30
+ try {
31
+ return Buffer.from(str, 'base64').toString('base64') === str;
32
+ } catch (err) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function isValidBase64(str) {
38
+ try {
39
+ Buffer.from(str, 'base64');
40
+ return true;
41
+ } catch (err) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function decodeScriptContent(script, format) {
47
+ if (format === 'base64') {
48
+ try {
49
+ return Buffer.from(script, 'base64').toString('utf8');
50
+ } catch (err) {
51
+ throw new Error('Failed to decode base64 script content');
52
+ }
53
+ }
54
+ return script;
55
+ }
56
+
26
57
  exports.listScripts = async (req, res) => {
27
58
  try {
28
59
  const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
@@ -45,42 +76,115 @@ exports.getScript = async (req, res) => {
45
76
  };
46
77
 
47
78
  exports.createScript = async (req, res) => {
79
+ let created = null;
48
80
  try {
81
+ console.log('[createScript] Starting script creation...');
82
+ console.log('[createScript] Request body keys:', Object.keys(req.body || {}));
83
+
49
84
  const payload = req.body || {};
85
+ console.log('[createScript] Payload name:', payload.name);
86
+ console.log('[createScript] Payload type:', payload.type);
87
+ console.log('[createScript] Payload runner:', payload.runner);
88
+ console.log('[createScript] Script length:', (payload.script || '').length);
89
+ console.log('[createScript] Script format:', payload.scriptFormat);
90
+
91
+ // Handle script content encoding
92
+ let scriptContent = String(payload.script || '');
93
+ let scriptFormat = payload.scriptFormat || 'string';
94
+
95
+ // Auto-detect base64 if not specified and content looks like base64
96
+ if (scriptFormat === 'string' && isBase64(scriptContent)) {
97
+ scriptFormat = 'base64';
98
+ console.log('[createScript] Auto-detected base64 format');
99
+ }
100
+
101
+ // Validate base64 content if format is base64
102
+ if (scriptFormat === 'base64' && !isValidBase64(scriptContent)) {
103
+ console.log('[createScript] Invalid base64 content detected');
104
+ throw new Error('Invalid base64 script content');
105
+ }
50
106
 
107
+ console.log('[createScript] About to create ScriptDefinition...');
51
108
  const doc = await ScriptDefinition.create({
52
109
  name: String(payload.name || '').trim(),
53
110
  codeIdentifier: String(payload.codeIdentifier || '').trim(),
54
111
  description: String(payload.description || ''),
55
112
  type: String(payload.type || '').trim(),
56
113
  runner: String(payload.runner || '').trim(),
57
- script: String(payload.script || ''),
114
+ script: scriptContent,
115
+ scriptFormat: scriptFormat,
58
116
  defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
59
117
  env: normalizeEnv(payload.env),
60
118
  timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
61
119
  enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
62
120
  });
121
+
122
+ console.log('[createScript] ScriptDefinition created successfully');
63
123
 
124
+ created = doc.toObject();
125
+ console.log('[createScript] About to create audit entry...');
126
+ console.log('[createScript] Script created successfully:', { name: created.name, id: created._id });
127
+
128
+ logAuditSync({
129
+ req,
130
+ action: 'scripts.create',
131
+ outcome: 'success',
132
+ entityType: 'ScriptDefinition',
133
+ entityId: created._id,
134
+ data: { name: created.name },
135
+ });
136
+
137
+ console.log('[createScript] About to send response...');
64
138
  res.status(201).json({ item: doc.toObject() });
139
+ console.log('[createScript] Response sent successfully');
65
140
  } catch (err) {
141
+ console.log('[createScript] ERROR occurred:', err);
142
+ console.log('[createScript] ERROR stack:', err.stack);
143
+ console.log('[createScript] ERROR message:', err.message);
144
+ console.log('[createScript] ERROR code:', err.code);
145
+
146
+ console.log('[createScript] Script creation failed:', { error: err?.message || 'Operation failed' });
66
147
  const safe = toSafeJsonError(err);
148
+ console.log('[createScript] Safe error:', safe);
67
149
  res.status(safe.status).json(safe.body);
68
150
  }
69
151
  };
70
152
 
71
153
  exports.updateScript = async (req, res) => {
154
+ let before = null;
155
+ let after = null;
72
156
  try {
73
157
  const payload = req.body || {};
74
158
 
75
159
  const doc = await ScriptDefinition.findById(req.params.id);
76
160
  if (!doc) return res.status(404).json({ error: 'Not found' });
77
161
 
162
+ before = doc.toObject();
163
+
164
+ // Handle script content encoding
165
+ if (payload.script !== undefined) {
166
+ let scriptContent = String(payload.script || '');
167
+ let scriptFormat = payload.scriptFormat || doc.scriptFormat || 'string';
168
+
169
+ // Auto-detect base64 if not specified and content looks like base64
170
+ if (scriptFormat === 'string' && isBase64(scriptContent)) {
171
+ scriptFormat = 'base64';
172
+ }
173
+
174
+ // Validate base64 content if format is base64
175
+ if (scriptFormat === 'base64' && !isValidBase64(scriptContent)) {
176
+ throw new Error('Invalid base64 script content');
177
+ }
178
+
179
+ doc.script = scriptContent;
180
+ doc.scriptFormat = scriptFormat;
181
+ }
182
+
78
183
  if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
79
184
  if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
80
185
  if (payload.description !== undefined) doc.description = String(payload.description || '');
81
186
  if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
82
187
  if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
83
- if (payload.script !== undefined) doc.script = String(payload.script || '');
84
188
  if (payload.defaultWorkingDirectory !== undefined) {
85
189
  doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
86
190
  }
@@ -89,34 +193,52 @@ exports.updateScript = async (req, res) => {
89
193
  if (payload.enabled !== undefined) doc.enabled = Boolean(payload.enabled);
90
194
 
91
195
  await doc.save();
196
+ after = doc.toObject();
197
+ console.log('[updateScript] Script updated successfully:', { name: after.name, id: after._id });
92
198
  res.json({ item: doc.toObject() });
93
199
  } catch (err) {
200
+ console.log('[updateScript] ERROR occurred:', err);
201
+ console.log('[updateScript] ERROR message:', err.message);
94
202
  const safe = toSafeJsonError(err);
95
203
  res.status(safe.status).json(safe.body);
96
204
  }
97
205
  };
98
206
 
99
207
  exports.deleteScript = async (req, res) => {
208
+ let before = null;
100
209
  try {
101
210
  const doc = await ScriptDefinition.findById(req.params.id);
102
211
  if (!doc) return res.status(404).json({ error: 'Not found' });
212
+ before = doc.toObject();
103
213
  await doc.deleteOne();
214
+
215
+ console.log('[deleteScript] Script deleted successfully:', { name: before.name, id: before._id });
104
216
  res.json({ ok: true });
105
217
  } catch (err) {
218
+ console.log('[deleteScript] ERROR occurred:', err);
219
+ console.log('[deleteScript] ERROR message:', err.message);
106
220
  const safe = toSafeJsonError(err);
107
221
  res.status(safe.status).json(safe.body);
108
222
  }
109
223
  };
110
224
 
111
225
  exports.runScript = async (req, res) => {
226
+ let script = null;
112
227
  try {
113
228
  const doc = await ScriptDefinition.findById(req.params.id);
114
229
  if (!doc) return res.status(404).json({ error: 'Not found' });
115
230
  if (!doc.enabled) return res.status(400).json({ error: 'Script is disabled' });
116
231
 
117
- const result = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
118
- res.json(result);
232
+ script = doc.toObject();
233
+
234
+ const runDoc = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
235
+
236
+ console.log('[runScript] Script executed successfully:', { name: script.name, runId: runDoc._id });
237
+
238
+ res.json({ runId: String(runDoc._id) });
119
239
  } catch (err) {
240
+ console.log('[runScript] ERROR occurred:', err);
241
+ console.log('[runScript] ERROR message:', err.message);
120
242
  const safe = toSafeJsonError(err);
121
243
  res.status(safe.status).json(safe.body);
122
244
  }
@@ -1,4 +1,3 @@
1
- const OpenAI = require('openai');
2
1
  const fs = require('fs');
3
2
  const path = require('path');
4
3
 
@@ -15,6 +14,9 @@ const {
15
14
  DEFAULT_OG_PNG_OUTPUT_PATH,
16
15
  } = require('../services/seoConfig.service');
17
16
 
17
+ const llmService = require('../services/llm.service');
18
+ const { resolveLlmProviderModel } = require('../services/llmDefaults.service');
19
+
18
20
  function handleServiceError(res, error) {
19
21
  const msg = error?.message || 'Operation failed';
20
22
  const code = error?.code;
@@ -231,6 +233,7 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
231
233
  const viewPath = String(req.body?.viewPath || '').trim();
232
234
  const routePath = validateRoutePathOrThrow(req.body?.routePath);
233
235
  const modelOverride = req.body?.model;
236
+ const providerKeyOverride = req.body?.providerKey;
234
237
 
235
238
  if (!viewPath || !viewPath.endsWith('.ejs')) {
236
239
  return res.status(400).json({ error: 'viewPath is required and must end with .ejs' });
@@ -250,12 +253,18 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
250
253
  return res.status(400).json({ error: 'view file is too large' });
251
254
  }
252
255
 
253
- const apiKey = await getSeoconfigOpenRouterApiKey();
254
- if (!apiKey) {
255
- return res.status(400).json({ error: 'AI is disabled (missing OpenRouter API key)' });
256
- }
256
+ const resolved = await resolveLlmProviderModel({
257
+ systemKey: 'seoConfig.entry.generate',
258
+ providerKey: providerKeyOverride,
259
+ model: modelOverride,
260
+ });
257
261
 
258
- const model = modelOverride || (await getSeoconfigOpenRouterModel());
262
+ const legacyApiKey = await getSeoconfigOpenRouterApiKey();
263
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
264
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
265
+ : {};
266
+
267
+ const model = resolved.model || (await getSeoconfigOpenRouterModel());
259
268
 
260
269
  const { data } = await getSeoConfigData();
261
270
  const siteName = data?.siteName || '';
@@ -263,11 +272,6 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
263
272
 
264
273
  const ejsSource = await fs.promises.readFile(abs, 'utf8');
265
274
 
266
- const client = new OpenAI({
267
- apiKey,
268
- baseURL: 'https://openrouter.ai/api/v1',
269
- });
270
-
271
275
  const prompt = buildSeoEntryPromptFromEjs({
272
276
  routePath,
273
277
  viewRelPath: viewPath,
@@ -276,15 +280,20 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
276
280
  baseUrl,
277
281
  });
278
282
 
279
- const resp = await client.chat.completions.create({
280
- model,
281
- messages: [{ role: 'user', content: prompt }],
282
- });
283
+ const resp = await llmService.callAdhoc(
284
+ {
285
+ providerKey: resolved.providerKey,
286
+ model,
287
+ messages: [{ role: 'user', content: prompt }],
288
+ promptKeyForAudit: 'seoConfig.entry.generate',
289
+ },
290
+ runtimeOptions,
291
+ );
283
292
 
284
- const out = resp.choices?.[0]?.message?.content || '';
293
+ const out = resp.content || '';
285
294
  const entry = parseAiJsonObjectOrThrow(out);
286
295
 
287
- return res.json({ routePath, entry, model });
296
+ return res.json({ routePath, entry, model, providerKey: resolved.providerKey });
288
297
  } catch (error) {
289
298
  const code = error?.code;
290
299
  if (code === 'VALIDATION') {
@@ -323,6 +332,7 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
323
332
  const routePath = validateRoutePathOrThrow(req.body?.routePath);
324
333
  const instruction = String(req.body?.instruction || '').trim();
325
334
  const modelOverride = req.body?.model;
335
+ const providerKeyOverride = req.body?.providerKey;
326
336
 
327
337
  if (!instruction) {
328
338
  return res.status(400).json({ error: 'instruction is required' });
@@ -331,12 +341,18 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
331
341
  return res.status(400).json({ error: 'instruction is too large' });
332
342
  }
333
343
 
334
- const apiKey = await getSeoconfigOpenRouterApiKey();
335
- if (!apiKey) {
336
- return res.status(400).json({ error: 'AI is disabled (missing OpenRouter API key)' });
337
- }
344
+ const resolved = await resolveLlmProviderModel({
345
+ systemKey: 'seoConfig.entry.improve',
346
+ providerKey: providerKeyOverride,
347
+ model: modelOverride,
348
+ });
338
349
 
339
- const model = modelOverride || (await getSeoconfigOpenRouterModel());
350
+ const legacyApiKey = await getSeoconfigOpenRouterApiKey();
351
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
352
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
353
+ : {};
354
+
355
+ const model = resolved.model || (await getSeoconfigOpenRouterModel());
340
356
 
341
357
  const { data } = await getSeoConfigData();
342
358
  const siteName = data?.siteName || '';
@@ -346,11 +362,6 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
346
362
  return res.status(404).json({ error: `No existing entry for ${routePath}` });
347
363
  }
348
364
 
349
- const client = new OpenAI({
350
- apiKey,
351
- baseURL: 'https://openrouter.ai/api/v1',
352
- });
353
-
354
365
  const prompt = buildSeoEntryPromptImprove({
355
366
  routePath,
356
367
  existingEntry,
@@ -359,15 +370,20 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
359
370
  baseUrl,
360
371
  });
361
372
 
362
- const resp = await client.chat.completions.create({
363
- model,
364
- messages: [{ role: 'user', content: prompt }],
365
- });
373
+ const resp = await llmService.callAdhoc(
374
+ {
375
+ providerKey: resolved.providerKey,
376
+ model,
377
+ messages: [{ role: 'user', content: prompt }],
378
+ promptKeyForAudit: 'seoConfig.entry.improve',
379
+ },
380
+ runtimeOptions,
381
+ );
366
382
 
367
- const out = resp.choices?.[0]?.message?.content || '';
383
+ const out = resp.content || '';
368
384
  const entry = parseAiJsonObjectOrThrow(out);
369
385
 
370
- return res.json({ routePath, entry, model });
386
+ return res.json({ routePath, entry, model, providerKey: resolved.providerKey });
371
387
  } catch (error) {
372
388
  const code = error?.code;
373
389
  if (code === 'VALIDATION') {
@@ -469,6 +485,7 @@ exports.aiEditSvg = async (req, res) => {
469
485
  const svgRaw = req.body?.svgRaw;
470
486
  const instruction = req.body?.instruction;
471
487
  const modelOverride = req.body?.model;
488
+ const providerKeyOverride = req.body?.providerKey;
472
489
 
473
490
  if (typeof svgRaw !== 'string' || svgRaw.trim() === '') {
474
491
  return res.status(400).json({ error: 'svgRaw is required' });
@@ -484,30 +501,36 @@ exports.aiEditSvg = async (req, res) => {
484
501
  return res.status(400).json({ error: 'instruction is too large' });
485
502
  }
486
503
 
487
- const apiKey = await getSeoconfigOpenRouterApiKey();
488
- if (!apiKey) {
489
- return res.status(400).json({ error: 'AI is disabled (missing OpenRouter API key)' });
490
- }
504
+ const resolved = await resolveLlmProviderModel({
505
+ systemKey: 'seoConfig.ogSvg.edit',
506
+ providerKey: providerKeyOverride,
507
+ model: modelOverride,
508
+ });
491
509
 
492
- const model = modelOverride || (await getSeoconfigOpenRouterModel());
510
+ const legacyApiKey = await getSeoconfigOpenRouterApiKey();
511
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
512
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
513
+ : {};
493
514
 
494
- const client = new OpenAI({
495
- apiKey,
496
- baseURL: 'https://openrouter.ai/api/v1',
497
- });
515
+ const model = resolved.model || (await getSeoconfigOpenRouterModel());
498
516
 
499
517
  const prompt = buildSvgAiPrompt({ svg: svgRaw, instruction });
500
- const resp = await client.chat.completions.create({
501
- model,
502
- messages: [{ role: 'user', content: prompt }],
503
- });
518
+ const resp = await llmService.callAdhoc(
519
+ {
520
+ providerKey: resolved.providerKey,
521
+ model,
522
+ messages: [{ role: 'user', content: prompt }],
523
+ promptKeyForAudit: 'seoConfig.ogSvg.edit',
524
+ },
525
+ runtimeOptions,
526
+ );
504
527
 
505
- const out = resp.choices?.[0]?.message?.content?.trim() || '';
528
+ const out = String(resp.content || '').trim();
506
529
  if (!out.startsWith('<svg') || !out.includes('</svg>')) {
507
530
  return res.status(500).json({ error: 'AI returned invalid SVG' });
508
531
  }
509
532
 
510
- return res.json({ svgRaw: out, model });
533
+ return res.json({ svgRaw: out, model, providerKey: resolved.providerKey });
511
534
  } catch (error) {
512
535
  console.error('Error editing SVG with AI:', error);
513
536
  return res.status(500).json({ error: error?.message || 'Failed to edit SVG' });