@aj-archipelago/cortex 1.3.62 → 1.3.63

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 (211) hide show
  1. package/.github/workflows/cortex-file-handler-test.yml +61 -0
  2. package/README.md +31 -7
  3. package/config/default.example.json +15 -0
  4. package/config.js +133 -12
  5. package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +22 -0
  6. package/helper-apps/cortex-autogen2/Dockerfile +31 -0
  7. package/helper-apps/cortex-autogen2/Dockerfile.worker +41 -0
  8. package/helper-apps/cortex-autogen2/README.md +183 -0
  9. package/helper-apps/cortex-autogen2/__init__.py +1 -0
  10. package/helper-apps/cortex-autogen2/agents.py +131 -0
  11. package/helper-apps/cortex-autogen2/docker-compose.yml +20 -0
  12. package/helper-apps/cortex-autogen2/function_app.py +55 -0
  13. package/helper-apps/cortex-autogen2/host.json +15 -0
  14. package/helper-apps/cortex-autogen2/main.py +126 -0
  15. package/helper-apps/cortex-autogen2/poetry.lock +3652 -0
  16. package/helper-apps/cortex-autogen2/pyproject.toml +36 -0
  17. package/helper-apps/cortex-autogen2/requirements.txt +20 -0
  18. package/helper-apps/cortex-autogen2/send_task.py +105 -0
  19. package/helper-apps/cortex-autogen2/services/__init__.py +1 -0
  20. package/helper-apps/cortex-autogen2/services/azure_queue.py +85 -0
  21. package/helper-apps/cortex-autogen2/services/redis_publisher.py +153 -0
  22. package/helper-apps/cortex-autogen2/task_processor.py +488 -0
  23. package/helper-apps/cortex-autogen2/tools/__init__.py +24 -0
  24. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +175 -0
  25. package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +601 -0
  26. package/helper-apps/cortex-autogen2/tools/coding_tools.py +72 -0
  27. package/helper-apps/cortex-autogen2/tools/download_tools.py +48 -0
  28. package/helper-apps/cortex-autogen2/tools/file_tools.py +545 -0
  29. package/helper-apps/cortex-autogen2/tools/search_tools.py +646 -0
  30. package/helper-apps/cortex-azure-cleaner/README.md +36 -0
  31. package/helper-apps/cortex-file-converter/README.md +93 -0
  32. package/helper-apps/cortex-file-converter/key_to_pdf.py +104 -0
  33. package/helper-apps/cortex-file-converter/list_blob_extensions.py +89 -0
  34. package/helper-apps/cortex-file-converter/process_azure_keynotes.py +181 -0
  35. package/helper-apps/cortex-file-converter/requirements.txt +1 -0
  36. package/helper-apps/cortex-file-handler/.env.test.azure.ci +7 -0
  37. package/helper-apps/cortex-file-handler/.env.test.azure.sample +1 -1
  38. package/helper-apps/cortex-file-handler/.env.test.gcs.ci +10 -0
  39. package/helper-apps/cortex-file-handler/.env.test.gcs.sample +2 -2
  40. package/helper-apps/cortex-file-handler/INTERFACE.md +41 -0
  41. package/helper-apps/cortex-file-handler/package.json +1 -1
  42. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +41 -17
  43. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +30 -15
  44. package/helper-apps/cortex-file-handler/scripts/test-azure.sh +32 -6
  45. package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +24 -2
  46. package/helper-apps/cortex-file-handler/scripts/validate-env.js +128 -0
  47. package/helper-apps/cortex-file-handler/src/blobHandler.js +161 -51
  48. package/helper-apps/cortex-file-handler/src/constants.js +3 -0
  49. package/helper-apps/cortex-file-handler/src/fileChunker.js +10 -8
  50. package/helper-apps/cortex-file-handler/src/index.js +116 -9
  51. package/helper-apps/cortex-file-handler/src/redis.js +61 -1
  52. package/helper-apps/cortex-file-handler/src/services/ConversionService.js +11 -8
  53. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +2 -2
  54. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +88 -6
  55. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +58 -0
  56. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +25 -5
  57. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +9 -0
  58. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +120 -16
  59. package/helper-apps/cortex-file-handler/src/start.js +27 -17
  60. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +52 -1
  61. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +40 -0
  62. package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +553 -0
  63. package/helper-apps/cortex-file-handler/tests/cleanup.test.js +46 -52
  64. package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +451 -0
  65. package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +229 -0
  66. package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +392 -0
  67. package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +7 -2
  68. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +348 -0
  69. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +23 -2
  70. package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +11 -5
  71. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +58 -24
  72. package/helper-apps/cortex-file-handler/tests/postOperations.test.js +11 -4
  73. package/helper-apps/cortex-file-handler/tests/shortLivedUrlConversion.test.js +225 -0
  74. package/helper-apps/cortex-file-handler/tests/start.test.js +8 -12
  75. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +80 -0
  76. package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +388 -22
  77. package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -0
  78. package/lib/cortexResponse.js +153 -0
  79. package/lib/entityConstants.js +21 -3
  80. package/lib/logger.js +21 -4
  81. package/lib/pathwayTools.js +28 -9
  82. package/lib/util.js +49 -0
  83. package/package.json +1 -1
  84. package/pathways/basePathway.js +1 -0
  85. package/pathways/bing_afagent.js +54 -1
  86. package/pathways/call_tools.js +2 -3
  87. package/pathways/chat_jarvis.js +1 -1
  88. package/pathways/google_cse.js +27 -0
  89. package/pathways/grok_live_search.js +18 -0
  90. package/pathways/system/entity/memory/sys_memory_lookup_required.js +1 -0
  91. package/pathways/system/entity/memory/sys_memory_required.js +1 -0
  92. package/pathways/system/entity/memory/sys_search_memory.js +1 -0
  93. package/pathways/system/entity/sys_entity_agent.js +56 -4
  94. package/pathways/system/entity/sys_generator_quick.js +1 -0
  95. package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +26 -0
  96. package/pathways/system/entity/tools/sys_tool_google_search.js +141 -0
  97. package/pathways/system/entity/tools/sys_tool_grok_x_search.js +237 -0
  98. package/pathways/system/entity/tools/sys_tool_image.js +1 -1
  99. package/pathways/system/rest_streaming/sys_claude_37_sonnet.js +21 -0
  100. package/pathways/system/rest_streaming/sys_claude_41_opus.js +21 -0
  101. package/pathways/system/rest_streaming/sys_claude_4_sonnet.js +21 -0
  102. package/pathways/system/rest_streaming/sys_google_gemini_25_flash.js +25 -0
  103. package/pathways/system/rest_streaming/{sys_google_gemini_chat.js → sys_google_gemini_25_pro.js} +6 -4
  104. package/pathways/system/rest_streaming/sys_grok_4.js +23 -0
  105. package/pathways/system/rest_streaming/sys_grok_4_fast_non_reasoning.js +23 -0
  106. package/pathways/system/rest_streaming/sys_grok_4_fast_reasoning.js +23 -0
  107. package/pathways/system/rest_streaming/sys_openai_chat.js +3 -0
  108. package/pathways/system/rest_streaming/sys_openai_chat_gpt41.js +22 -0
  109. package/pathways/system/rest_streaming/sys_openai_chat_gpt41_mini.js +21 -0
  110. package/pathways/system/rest_streaming/sys_openai_chat_gpt41_nano.js +21 -0
  111. package/pathways/system/rest_streaming/{sys_claude_35_sonnet.js → sys_openai_chat_gpt4_omni.js} +6 -4
  112. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_omni_mini.js +21 -0
  113. package/pathways/system/rest_streaming/{sys_claude_3_haiku.js → sys_openai_chat_gpt5.js} +7 -5
  114. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_chat.js +21 -0
  115. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_mini.js +21 -0
  116. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_nano.js +21 -0
  117. package/pathways/system/rest_streaming/{sys_openai_chat_o1.js → sys_openai_chat_o3.js} +6 -3
  118. package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +3 -0
  119. package/pathways/system/workspaces/run_workspace_prompt.js +99 -0
  120. package/pathways/vision.js +1 -1
  121. package/server/graphql.js +1 -1
  122. package/server/modelExecutor.js +8 -0
  123. package/server/pathwayResolver.js +166 -16
  124. package/server/pathwayResponseParser.js +16 -8
  125. package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
  126. package/server/plugins/claude3VertexPlugin.js +193 -45
  127. package/server/plugins/gemini15ChatPlugin.js +21 -0
  128. package/server/plugins/gemini15VisionPlugin.js +360 -0
  129. package/server/plugins/googleCsePlugin.js +94 -0
  130. package/server/plugins/grokVisionPlugin.js +365 -0
  131. package/server/plugins/modelPlugin.js +3 -1
  132. package/server/plugins/openAiChatPlugin.js +106 -13
  133. package/server/plugins/openAiVisionPlugin.js +42 -30
  134. package/server/resolver.js +28 -4
  135. package/server/rest.js +270 -53
  136. package/server/typeDef.js +1 -0
  137. package/tests/{mocks.js → helpers/mocks.js} +5 -2
  138. package/tests/{server.js → helpers/server.js} +2 -2
  139. package/tests/helpers/sseAssert.js +23 -0
  140. package/tests/helpers/sseClient.js +73 -0
  141. package/tests/helpers/subscriptionAssert.js +11 -0
  142. package/tests/helpers/subscriptions.js +113 -0
  143. package/tests/{sublong.srt → integration/features/translate/sublong.srt} +4543 -4543
  144. package/tests/integration/features/translate/translate_chunking_stream.test.js +100 -0
  145. package/tests/{translate_srt.test.js → integration/features/translate/translate_srt.test.js} +2 -2
  146. package/tests/integration/graphql/async/stream/agentic.test.js +477 -0
  147. package/tests/integration/graphql/async/stream/subscription_streaming.test.js +62 -0
  148. package/tests/integration/graphql/async/stream/sys_entity_start_streaming.test.js +71 -0
  149. package/tests/integration/graphql/async/stream/vendors/claude_streaming.test.js +56 -0
  150. package/tests/integration/graphql/async/stream/vendors/gemini_streaming.test.js +66 -0
  151. package/tests/integration/graphql/async/stream/vendors/grok_streaming.test.js +56 -0
  152. package/tests/integration/graphql/async/stream/vendors/openai_streaming.test.js +72 -0
  153. package/tests/integration/graphql/features/google/sysToolGoogleSearch.test.js +96 -0
  154. package/tests/integration/graphql/features/grok/grok.test.js +688 -0
  155. package/tests/integration/graphql/features/grok/grok_x_search_tool.test.js +354 -0
  156. package/tests/{main.test.js → integration/graphql/features/main.test.js} +1 -1
  157. package/tests/{call_tools.test.js → integration/graphql/features/tools/call_tools.test.js} +2 -2
  158. package/tests/{vision.test.js → integration/graphql/features/vision/vision.test.js} +1 -1
  159. package/tests/integration/graphql/subscriptions/connection.test.js +26 -0
  160. package/tests/{openai_api.test.js → integration/rest/oai/openai_api.test.js} +63 -238
  161. package/tests/integration/rest/oai/tool_calling_api.test.js +343 -0
  162. package/tests/integration/rest/oai/tool_calling_streaming.test.js +85 -0
  163. package/tests/integration/rest/vendors/claude_streaming.test.js +47 -0
  164. package/tests/integration/rest/vendors/claude_tool_calling_streaming.test.js +75 -0
  165. package/tests/integration/rest/vendors/gemini_streaming.test.js +47 -0
  166. package/tests/integration/rest/vendors/gemini_tool_calling_streaming.test.js +75 -0
  167. package/tests/integration/rest/vendors/grok_streaming.test.js +55 -0
  168. package/tests/integration/rest/vendors/grok_tool_calling_streaming.test.js +75 -0
  169. package/tests/{azureAuthTokenHelper.test.js → unit/core/azureAuthTokenHelper.test.js} +1 -1
  170. package/tests/{chunkfunction.test.js → unit/core/chunkfunction.test.js} +2 -2
  171. package/tests/{config.test.js → unit/core/config.test.js} +3 -3
  172. package/tests/{encodeCache.test.js → unit/core/encodeCache.test.js} +1 -1
  173. package/tests/{fastLruCache.test.js → unit/core/fastLruCache.test.js} +1 -1
  174. package/tests/{handleBars.test.js → unit/core/handleBars.test.js} +1 -1
  175. package/tests/{memoryfunction.test.js → unit/core/memoryfunction.test.js} +2 -2
  176. package/tests/unit/core/mergeResolver.test.js +952 -0
  177. package/tests/{parser.test.js → unit/core/parser.test.js} +3 -3
  178. package/tests/unit/core/pathwayResolver.test.js +187 -0
  179. package/tests/{requestMonitor.test.js → unit/core/requestMonitor.test.js} +1 -1
  180. package/tests/{requestMonitorDurationEstimator.test.js → unit/core/requestMonitorDurationEstimator.test.js} +1 -1
  181. package/tests/{truncateMessages.test.js → unit/core/truncateMessages.test.js} +3 -3
  182. package/tests/{util.test.js → unit/core/util.test.js} +1 -1
  183. package/tests/{apptekTranslatePlugin.test.js → unit/plugins/apptekTranslatePlugin.test.js} +3 -3
  184. package/tests/{azureFoundryAgents.test.js → unit/plugins/azureFoundryAgents.test.js} +136 -1
  185. package/tests/{claude3VertexPlugin.test.js → unit/plugins/claude3VertexPlugin.test.js} +32 -10
  186. package/tests/{claude3VertexToolConversion.test.js → unit/plugins/claude3VertexToolConversion.test.js} +3 -3
  187. package/tests/unit/plugins/googleCsePlugin.test.js +111 -0
  188. package/tests/unit/plugins/grokVisionPlugin.test.js +1392 -0
  189. package/tests/{modelPlugin.test.js → unit/plugins/modelPlugin.test.js} +3 -3
  190. package/tests/{multimodal_conversion.test.js → unit/plugins/multimodal_conversion.test.js} +4 -4
  191. package/tests/{openAiChatPlugin.test.js → unit/plugins/openAiChatPlugin.test.js} +13 -4
  192. package/tests/{openAiToolPlugin.test.js → unit/plugins/openAiToolPlugin.test.js} +35 -27
  193. package/tests/{tokenHandlingTests.test.js → unit/plugins/tokenHandlingTests.test.js} +5 -5
  194. package/tests/{translate_apptek.test.js → unit/plugins/translate_apptek.test.js} +3 -3
  195. package/tests/{streaming.test.js → unit/plugins.streaming/plugin_stream_events.test.js} +19 -58
  196. package/helper-apps/mogrt-handler/tests/test-files/test.gif +0 -1
  197. package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +0 -1
  198. package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +0 -1
  199. package/pathways/system/rest_streaming/sys_openai_chat_gpt4.js +0 -19
  200. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_32.js +0 -19
  201. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_turbo.js +0 -19
  202. package/pathways/system/workspaces/run_claude35_sonnet.js +0 -21
  203. package/pathways/system/workspaces/run_claude3_haiku.js +0 -20
  204. package/pathways/system/workspaces/run_gpt35turbo.js +0 -20
  205. package/pathways/system/workspaces/run_gpt4.js +0 -20
  206. package/pathways/system/workspaces/run_gpt4_32.js +0 -20
  207. package/tests/agentic.test.js +0 -256
  208. package/tests/pathwayResolver.test.js +0 -78
  209. package/tests/subscription.test.js +0 -387
  210. /package/tests/{subchunk.srt → integration/features/translate/subchunk.srt} +0 -0
  211. /package/tests/{subhorizontal.srt → integration/features/translate/subhorizontal.srt} +0 -0
@@ -3,7 +3,7 @@ import os from "os";
3
3
  import path from "path";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
 
6
- import { DOC_EXTENSIONS } from "./constants.js";
6
+ import { DOC_EXTENSIONS, AZURITE_ACCOUNT_NAME } from "./constants.js";
7
7
  import { easyChunker } from "./docHelper.js";
8
8
  import { downloadFile, splitMediaFile } from "./fileChunker.js";
9
9
  import { ensureEncoded, ensureFileExtension, urlExists } from "./helper.js";
@@ -53,17 +53,22 @@ async function CortexFileHandler(context, req) {
53
53
  hash,
54
54
  checkHash,
55
55
  clearHash,
56
+ shortLivedMinutes,
56
57
  fetch,
57
58
  load,
58
59
  restore,
60
+ container,
59
61
  } = req.body?.params || req.query;
60
62
 
61
63
  // Normalize boolean parameters
62
64
  const shouldSave = save === true || save === "true";
63
65
  const shouldCheckHash = checkHash === true || checkHash === "true";
64
66
  const shouldClearHash = clearHash === true || clearHash === "true";
67
+ const shortLivedDuration = parseInt(shortLivedMinutes) || 5; // Default to 5 minutes
65
68
  const shouldFetchRemote = fetch || load || restore;
66
69
 
70
+
71
+
67
72
  const operation = shouldSave
68
73
  ? "save"
69
74
  : shouldCheckHash
@@ -90,9 +95,11 @@ async function CortexFileHandler(context, req) {
90
95
 
91
96
  // Initialize services
92
97
  const storageService = new StorageService();
98
+ await storageService._initialize(); // Ensure providers are initialized
93
99
  const conversionService = new FileConversionService(
94
100
  context,
95
101
  storageService.primaryProvider.constructor.name === "AzureStorageProvider",
102
+ null,
96
103
  );
97
104
 
98
105
  // Validate URL for document processing and media chunking operations
@@ -124,13 +131,39 @@ async function CortexFileHandler(context, req) {
124
131
  }
125
132
 
126
133
  // Clean up files when request delete which means processing marked completed
134
+ // Supports two modes:
135
+ // 1. Delete multiple files by requestId (existing behavior)
136
+ // 2. Delete single file by hash (new behavior)
127
137
  if (operation === "delete") {
128
138
  const deleteRequestId = req.query.requestId || requestId;
129
139
  const deleteHash = req.query.hash || hash;
140
+
141
+ // If only hash is provided, delete single file by hash
142
+ if (deleteHash && !deleteRequestId) {
143
+ try {
144
+ const deleted = await storageService.deleteFileByHash(deleteHash);
145
+ context.res = {
146
+ status: 200,
147
+ body: {
148
+ message: `File with hash ${deleteHash} deleted successfully`,
149
+ deleted
150
+ },
151
+ };
152
+ return;
153
+ } catch (error) {
154
+ context.res = {
155
+ status: 404,
156
+ body: error.message,
157
+ };
158
+ return;
159
+ }
160
+ }
161
+
162
+ // If requestId is provided, use the existing multi-file delete flow
130
163
  if (!deleteRequestId) {
131
164
  context.res = {
132
165
  status: 400,
133
- body: "Please pass a requestId on the query string",
166
+ body: "Please pass either a requestId or hash on the query string",
134
167
  };
135
168
  return;
136
169
  }
@@ -167,15 +200,16 @@ async function CortexFileHandler(context, req) {
167
200
  return;
168
201
  }
169
202
 
170
- // Check if file already exists (using hash as the key)
171
- const exists = await getFileStoreMap(remoteUrl);
203
+ // Check if file already exists (using hash or URL as the key)
204
+ const cacheKey = hash || remoteUrl;
205
+ const exists = await getFileStoreMap(cacheKey);
172
206
  if (exists) {
173
207
  context.res = {
174
208
  status: 200,
175
209
  body: exists,
176
210
  };
177
211
  //update redis timestamp with current time
178
- await setFileStoreMap(remoteUrl, exists);
212
+ await setFileStoreMap(cacheKey, exists);
179
213
  return;
180
214
  }
181
215
 
@@ -190,10 +224,10 @@ async function CortexFileHandler(context, req) {
190
224
 
191
225
  // For remote files, we don't need a requestId folder structure since it's just a single file
192
226
  // Pass empty string to store the file directly in the root
193
- const res = await storageService.uploadFile(context, filename, '');
227
+ const res = await storageService.uploadFile(context, filename, '', null, null, container);
194
228
 
195
- //Update Redis (using hash as the key)
196
- await setFileStoreMap(remoteUrl, res);
229
+ //Update Redis (using hash or URL as the key)
230
+ await setFileStoreMap(cacheKey, res);
197
231
 
198
232
  // Return the file URL
199
233
  context.res = {
@@ -322,6 +356,9 @@ async function CortexFileHandler(context, req) {
322
356
  context,
323
357
  downloadedFile,
324
358
  hash,
359
+ null,
360
+ null,
361
+ container,
325
362
  );
326
363
 
327
364
  // Update the hash result with the new primary storage URL
@@ -382,6 +419,7 @@ async function CortexFileHandler(context, req) {
382
419
  hashResult = await conversionService.ensureConvertedVersion(
383
420
  hashResult,
384
421
  requestId,
422
+ container,
385
423
  );
386
424
  } catch (error) {
387
425
  context.log(`Error ensuring converted version: ${error}`);
@@ -395,6 +433,70 @@ async function CortexFileHandler(context, req) {
395
433
  };
396
434
  }
397
435
 
436
+ // Always generate short-lived URL for checkHash operations
437
+ // Use converted URL if available, otherwise use original URL
438
+ const urlForShortLived = hashResult.converted?.url || hashResult.url;
439
+ try {
440
+ // Extract blob name from the URL to generate new SAS token
441
+ let blobName;
442
+ try {
443
+ const url = new URL(urlForShortLived);
444
+ // Extract blob name from the URL path (remove leading slash)
445
+ let path = url.pathname.substring(1);
446
+
447
+ // For Azurite URLs, the path includes account name: devstoreaccount1/container/blob
448
+ // For real Azure URLs, the path is: container/blob
449
+ const containerName = storageService.primaryProvider.containerName;
450
+
451
+ // Check if this is an Azurite URL (contains devstoreaccount1)
452
+ if (path.startsWith(`${AZURITE_ACCOUNT_NAME}/`)) {
453
+ path = path.substring(`${AZURITE_ACCOUNT_NAME}/`.length); // Remove account prefix
454
+ }
455
+
456
+ // Now remove container prefix if it exists
457
+ if (path.startsWith(containerName + '/')) {
458
+ blobName = path.substring(containerName.length + 1);
459
+ } else {
460
+ blobName = path;
461
+ }
462
+
463
+ } catch (urlError) {
464
+ context.log(`Error parsing URL for short-lived generation: ${urlError}`);
465
+ }
466
+
467
+ // Generate short-lived SAS token
468
+ if (blobName && storageService.primaryProvider.generateShortLivedSASToken) {
469
+ const { containerClient } = await storageService.primaryProvider.getBlobClient();
470
+ const sasToken = storageService.primaryProvider.generateShortLivedSASToken(
471
+ containerClient,
472
+ blobName,
473
+ shortLivedDuration
474
+ );
475
+
476
+ // Construct new URL with short-lived SAS token
477
+ const baseUrl = urlForShortLived.split('?')[0]; // Remove existing SAS token
478
+ const shortLivedUrl = `${baseUrl}?${sasToken}`;
479
+
480
+ // Add short-lived URL to response
481
+ response.shortLivedUrl = shortLivedUrl;
482
+ response.expiresInMinutes = shortLivedDuration;
483
+
484
+ const urlType = hashResult.converted?.url ? 'converted' : 'original';
485
+ context.log(`Generated short-lived URL for hash: ${hash} using ${urlType} URL (expires in ${shortLivedDuration} minutes)`);
486
+ } else {
487
+ // Fallback for storage providers that don't support short-lived tokens
488
+ response.shortLivedUrl = urlForShortLived;
489
+ response.expiresInMinutes = shortLivedDuration;
490
+ const urlType = hashResult.converted?.url ? 'converted' : 'original';
491
+ context.log(`Storage provider doesn't support short-lived tokens, using ${urlType} URL`);
492
+ }
493
+ } catch (error) {
494
+ context.log(`Error generating short-lived URL: ${error}`);
495
+ // Provide fallback even on error
496
+ response.shortLivedUrl = urlForShortLived;
497
+ response.expiresInMinutes = shortLivedDuration;
498
+ }
499
+
398
500
  //update redis timestamp with current time
399
501
  await setFileStoreMap(hash, hashResult);
400
502
 
@@ -428,7 +530,7 @@ async function CortexFileHandler(context, req) {
428
530
  storageService.primaryProvider.constructor.name ===
429
531
  "LocalStorageProvider";
430
532
  // Use uploadBlob to handle multipart/form-data
431
- const result = await uploadBlob(context, req, saveToLocal, null, hash);
533
+ const result = await uploadBlob(context, req, saveToLocal, null, hash, container);
432
534
  if (result?.hash && context?.res?.body) {
433
535
  await setFileStoreMap(result.hash, context.res.body);
434
536
  }
@@ -496,6 +598,8 @@ async function CortexFileHandler(context, req) {
496
598
  await conversionService._saveConvertedFile(
497
599
  conversion.convertedPath,
498
600
  requestId,
601
+ null,
602
+ container,
499
603
  );
500
604
 
501
605
  // Return the converted file URL
@@ -511,6 +615,8 @@ async function CortexFileHandler(context, req) {
511
615
  const saveResult = await conversionService._saveConvertedFile(
512
616
  downloadedFile,
513
617
  requestId,
618
+ null,
619
+ container,
514
620
  );
515
621
 
516
622
  // Return the original file URL
@@ -592,6 +698,7 @@ async function CortexFileHandler(context, req) {
592
698
  requestId,
593
699
  null,
594
700
  chunkFilename,
701
+ container,
595
702
  );
596
703
 
597
704
  const chunkOffset = chunkOffsets[index];
@@ -1,6 +1,65 @@
1
1
  import redis from "ioredis";
2
+
2
3
  const connectionString = process.env["REDIS_CONNECTION_STRING"];
3
- const client = redis.createClient(connectionString);
4
+
5
+ // Create a mock client for test environment when Redis is not configured
6
+ const createMockClient = () => {
7
+ const store = new Map();
8
+ const hashMap = new Map();
9
+
10
+ return {
11
+ connected: false,
12
+ async connect() { return Promise.resolve(); },
13
+ async publish() { return Promise.resolve(); },
14
+ async hgetall(hashName) {
15
+ const hash = hashMap.get(hashName);
16
+ return hash ? Object.fromEntries(hash) : {};
17
+ },
18
+ async hset(hashName, key, value) {
19
+ if (!hashMap.has(hashName)) {
20
+ hashMap.set(hashName, new Map());
21
+ }
22
+ hashMap.get(hashName).set(key, value);
23
+ return Promise.resolve();
24
+ },
25
+ async hget(hashName, key) {
26
+ const hash = hashMap.get(hashName);
27
+ return hash ? hash.get(key) || null : null;
28
+ },
29
+ async hdel(hashName, key) {
30
+ const hash = hashMap.get(hashName);
31
+ if (hash && hash.has(key)) {
32
+ hash.delete(key);
33
+ return 1;
34
+ }
35
+ return 0;
36
+ },
37
+ async eval(script, numKeys, ...args) {
38
+ // Mock implementation for atomic get-and-delete operation
39
+ if (script.includes('hget') && script.includes('hdel')) {
40
+ const hashName = args[0];
41
+ const key = args[1];
42
+ const hash = hashMap.get(hashName);
43
+ if (hash && hash.has(key)) {
44
+ const value = hash.get(key);
45
+ hash.delete(key);
46
+ return value;
47
+ }
48
+ return null;
49
+ }
50
+ throw new Error('Mock eval only supports atomic get-and-delete');
51
+ },
52
+ };
53
+ };
54
+
55
+ // Only create real Redis client if connection string is provided
56
+ let client;
57
+ if (connectionString && process.env.NODE_ENV !== 'test') {
58
+ client = redis.createClient(connectionString);
59
+ } else {
60
+ console.log('Using mock Redis client for tests or missing connection string');
61
+ client = createMockClient();
62
+ }
4
63
 
5
64
  const channel = "requestProgress";
6
65
 
@@ -222,4 +281,5 @@ export {
222
281
  removeFromFileStoreMap,
223
282
  cleanupRedisFileStoreMap,
224
283
  cleanupRedisFileStoreMapAge,
284
+ client,
225
285
  };
@@ -9,11 +9,7 @@ import { CONVERTED_EXTENSIONS } from "../constants.js";
9
9
  import { v4 as uuidv4 } from "uuid";
10
10
  import { sanitizeFilename, generateShortId } from "../utils/filenameUtils.js";
11
11
 
12
- const MARKITDOWN_CONVERT_URL = process.env.MARKITDOWN_CONVERT_URL;
13
-
14
- if (!MARKITDOWN_CONVERT_URL) {
15
- throw new Error("MARKITDOWN_CONVERT_URL is not set");
16
- }
12
+ const MARKITDOWN_CONVERT_URL = process.env.MARKITDOWN_CONVERT_URL || null;
17
13
 
18
14
  export class ConversionService {
19
15
  constructor(context) {
@@ -100,9 +96,10 @@ export class ConversionService {
100
96
  * Ensures a file has both original and converted versions
101
97
  * @param {Object} fileInfo - Information about the file
102
98
  * @param {string} requestId - Request ID for storage
99
+ * @param {string} containerName - Optional container name for storage
103
100
  * @returns {Promise<Object>} - Updated file info with conversion if needed
104
101
  */
105
- async ensureConvertedVersion(fileInfo, requestId) {
102
+ async ensureConvertedVersion(fileInfo, requestId, containerName = null) {
106
103
  const { url, gcs } = fileInfo;
107
104
  // Remove any query parameters before extension check
108
105
  const extension = path.extname(url.split("?")[0]).toLowerCase();
@@ -162,6 +159,8 @@ export class ConversionService {
162
159
  const convertedSaveResult = await this._saveConvertedFile(
163
160
  conversion.convertedPath,
164
161
  requestId,
162
+ null,
163
+ containerName,
165
164
  );
166
165
  if (!convertedSaveResult) {
167
166
  throw new Error("Failed to save converted file to primary storage");
@@ -257,7 +256,11 @@ export class ConversionService {
257
256
 
258
257
  async _convertToMarkdown(fileUrl) {
259
258
  try {
260
- const apiUrl = `${MARKITDOWN_CONVERT_URL}${encodeURIComponent(fileUrl)}`;
259
+ const markitdownUrl = process.env.MARKITDOWN_CONVERT_URL;
260
+ if (!markitdownUrl) {
261
+ throw new Error("MARKITDOWN_CONVERT_URL is not set");
262
+ }
263
+ const apiUrl = `${markitdownUrl}${encodeURIComponent(fileUrl)}`;
261
264
  const response = await axios.get(apiUrl);
262
265
  return response.data.markdown || "";
263
266
  } catch (err) {
@@ -302,7 +305,7 @@ export class ConversionService {
302
305
  throw new Error("Method _downloadFile must be implemented");
303
306
  }
304
307
 
305
- async _saveConvertedFile(filePath, requestId) {
308
+ async _saveConvertedFile(filePath, requestId, filename = null, containerName = null) {
306
309
  throw new Error("Method _saveConvertedFile must be implemented");
307
310
  }
308
311
 
@@ -33,13 +33,13 @@ export class FileConversionService extends ConversionService {
33
33
  return downloadFile(url, destination);
34
34
  }
35
35
 
36
- async _saveConvertedFile(filePath, requestId, filename = null) {
36
+ async _saveConvertedFile(filePath, requestId, filename = null, containerName = null) {
37
37
  // Generate a fallback requestId if none supplied (e.g. during checkHash calls)
38
38
  const reqId = requestId || uuidv4();
39
39
 
40
40
  let fileUrl;
41
41
  if (this.useAzure) {
42
- const savedBlob = await saveFileToBlob(filePath, reqId, filename);
42
+ const savedBlob = await saveFileToBlob(filePath, reqId, filename, containerName);
43
43
  fileUrl = savedBlob.url;
44
44
  } else {
45
45
  fileUrl = await moveFileToPublicFolder(filePath, reqId);
@@ -7,6 +7,7 @@ import fs from "fs";
7
7
  import path from "path";
8
8
 
9
9
  import { StorageProvider } from "./StorageProvider.js";
10
+ import { AZURITE_ACCOUNT_NAME } from "../../constants.js";
10
11
  import {
11
12
  generateShortId,
12
13
  generateBlobName,
@@ -43,21 +44,54 @@ export class AzureStorageProvider extends StorageProvider {
43
44
  return { blobServiceClient, containerClient };
44
45
  }
45
46
 
46
- generateSASToken(containerClient, blobName) {
47
- const { accountName, accountKey } = containerClient.credential;
47
+ generateSASToken(containerClient, blobName, options = {}) {
48
+ // Handle Azurite (development storage) credentials
49
+ let accountName, accountKey;
50
+
51
+ // Note: Debug logging removed for production
52
+
53
+ if (containerClient.credential && containerClient.credential.accountName) {
54
+ // Regular Azure Storage credentials
55
+ accountName = containerClient.credential.accountName;
56
+
57
+ // Handle Buffer case (Azurite) vs string case (real Azure)
58
+ if (Buffer.isBuffer(containerClient.credential.accountKey)) {
59
+ accountKey = containerClient.credential.accountKey.toString('base64');
60
+ } else {
61
+ accountKey = containerClient.credential.accountKey;
62
+ }
63
+ } else {
64
+ // Azurite development storage fallback
65
+ accountName = AZURITE_ACCOUNT_NAME;
66
+ accountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
67
+ }
68
+
48
69
  const sharedKeyCredential = new StorageSharedKeyCredential(
49
70
  accountName,
50
71
  accountKey,
51
72
  );
52
73
 
74
+ // Support custom duration: minutes, hours, or fall back to default days
75
+ let expirationTime;
76
+ if (options.minutes) {
77
+ expirationTime = new Date(new Date().valueOf() + options.minutes * 60 * 1000);
78
+ } else if (options.hours) {
79
+ expirationTime = new Date(new Date().valueOf() + options.hours * 60 * 60 * 1000);
80
+ } else if (options.days) {
81
+ expirationTime = new Date(new Date().valueOf() + options.days * 24 * 60 * 60 * 1000);
82
+ } else {
83
+ // Default to configured sasTokenLifeDays
84
+ expirationTime = new Date(
85
+ new Date().valueOf() + this.sasTokenLifeDays * 24 * 60 * 60 * 1000,
86
+ );
87
+ }
88
+
53
89
  const sasOptions = {
54
90
  containerName: containerClient.containerName,
55
91
  blobName: blobName,
56
- permissions: "r",
92
+ permissions: options.permissions || "r",
57
93
  startsOn: new Date(),
58
- expiresOn: new Date(
59
- new Date().valueOf() + this.sasTokenLifeDays * 24 * 60 * 60 * 1000,
60
- ),
94
+ expiresOn: expirationTime,
61
95
  };
62
96
 
63
97
  return generateBlobSASQueryParameters(
@@ -66,6 +100,10 @@ export class AzureStorageProvider extends StorageProvider {
66
100
  ).toString();
67
101
  }
68
102
 
103
+ generateShortLivedSASToken(containerClient, blobName, minutes = 5) {
104
+ return this.generateSASToken(containerClient, blobName, { minutes });
105
+ }
106
+
69
107
  async uploadFile(context, filePath, requestId, hash = null, filename = null) {
70
108
  const { containerClient } = await this.getBlobClient();
71
109
 
@@ -123,6 +161,50 @@ export class AzureStorageProvider extends StorageProvider {
123
161
  return result;
124
162
  }
125
163
 
164
+ async deleteFile(url) {
165
+ if (!url) throw new Error("Missing URL parameter");
166
+
167
+ try {
168
+ const { containerClient } = await this.getBlobClient();
169
+
170
+ // Extract blob name from URL
171
+ const urlObj = new URL(url);
172
+ let blobName = urlObj.pathname.substring(1); // Remove leading slash
173
+
174
+ // Handle Azurite URLs which include account name in path: /devstoreaccount1/container/blob
175
+ if (blobName.includes('/')) {
176
+ const pathSegments = blobName.split('/');
177
+ if (pathSegments.length >= 2) {
178
+ // For Azurite: devstoreaccount1/container/blobname -> blobname
179
+ // Skip the account and container segments to get the actual blob name
180
+ blobName = pathSegments.slice(2).join('/');
181
+ }
182
+ }
183
+
184
+ // Remove container name prefix if present (for non-Azurite URLs)
185
+ if (blobName.startsWith(this.containerName + '/')) {
186
+ blobName = blobName.substring(this.containerName.length + 1);
187
+ }
188
+
189
+ const blockBlobClient = containerClient.getBlockBlobClient(blobName);
190
+
191
+ try {
192
+ await blockBlobClient.delete();
193
+ return blobName;
194
+ } catch (error) {
195
+ if (error.statusCode === 404) {
196
+ console.warn(`Azure blob not found during delete: ${blobName}`);
197
+ return null;
198
+ } else {
199
+ throw error;
200
+ }
201
+ }
202
+ } catch (error) {
203
+ console.error("Error deleting Azure blob:", error);
204
+ throw error;
205
+ }
206
+ }
207
+
126
208
  async fileExists(url) {
127
209
  try {
128
210
  // First attempt a lightweight HEAD request
@@ -153,6 +153,64 @@ export class GCSStorageProvider extends StorageProvider {
153
153
  }
154
154
  }
155
155
 
156
+ async deleteFile(url) {
157
+ if (!url) throw new Error("Missing URL parameter");
158
+
159
+ try {
160
+ if (!url.startsWith("gs://")) {
161
+ throw new Error("Invalid GCS URL format");
162
+ }
163
+
164
+ const unencodedUrl = this.ensureUnencodedGcsUrl(url);
165
+ const urlParts = unencodedUrl.replace("gs://", "").split("/");
166
+ const bucketName = urlParts[0];
167
+ const fileName = urlParts.slice(1).join("/");
168
+
169
+ if (process.env.STORAGE_EMULATOR_HOST) {
170
+ // When using the emulator, use raw REST API
171
+ try {
172
+ const response = await axios.delete(
173
+ `${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucketName}/o/${encodeURIComponent(fileName)}`,
174
+ {
175
+ validateStatus: (status) => status === 200 || status === 404,
176
+ }
177
+ );
178
+
179
+ if (response.status === 200) {
180
+ return fileName;
181
+ } else if (response.status === 404) {
182
+ console.warn(`GCS file not found during delete: ${fileName}`);
183
+ return null;
184
+ }
185
+ } catch (error) {
186
+ if (error.response?.status === 404) {
187
+ console.warn(`GCS file not found during delete: ${fileName}`);
188
+ return null;
189
+ }
190
+ throw error;
191
+ }
192
+ } else {
193
+ // Real GCS - use client library
194
+ const bucket = this.storage.bucket(bucketName);
195
+ const file = bucket.file(fileName);
196
+
197
+ try {
198
+ await file.delete();
199
+ return fileName;
200
+ } catch (error) {
201
+ if (error.code === 404) {
202
+ console.warn(`GCS file not found during delete: ${fileName}`);
203
+ return null;
204
+ }
205
+ throw error;
206
+ }
207
+ }
208
+ } catch (error) {
209
+ console.error("Error deleting GCS file:", error);
210
+ throw error;
211
+ }
212
+ }
213
+
156
214
  async fileExists(url) {
157
215
  try {
158
216
  if (!url || !url.startsWith("gs://")) {
@@ -4,24 +4,44 @@ import { LocalStorageProvider } from "./LocalStorageProvider.js";
4
4
  import path from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
+ // Lazy-load blob handler constants to avoid blocking module import
8
+ let blobHandlerConstants = null;
9
+ async function getBlobHandlerConstants() {
10
+ if (!blobHandlerConstants) {
11
+ blobHandlerConstants = await import("../../blobHandler.js");
12
+ }
13
+ return blobHandlerConstants;
14
+ }
15
+
7
16
  export class StorageFactory {
8
17
  constructor() {
9
18
  this.providers = new Map();
10
19
  }
11
20
 
12
- getPrimaryProvider() {
21
+ async getPrimaryProvider(containerName = null) {
13
22
  if (process.env.AZURE_STORAGE_CONNECTION_STRING) {
14
- return this.getAzureProvider();
23
+ return await this.getAzureProvider(containerName);
15
24
  }
16
25
  return this.getLocalProvider();
17
26
  }
18
27
 
19
- getAzureProvider() {
20
- const key = "azure";
28
+ async getAzureProvider(containerName = null) {
29
+ const { AZURE_STORAGE_CONTAINER_NAMES, DEFAULT_AZURE_STORAGE_CONTAINER_NAME, isValidContainerName } = await getBlobHandlerConstants();
30
+
31
+ // Use provided container name or default to first in whitelist
32
+ const finalContainerName = containerName || DEFAULT_AZURE_STORAGE_CONTAINER_NAME;
33
+
34
+ // Validate container name
35
+ if (!isValidContainerName(finalContainerName)) {
36
+ throw new Error(`Invalid container name '${finalContainerName}'. Allowed containers: ${AZURE_STORAGE_CONTAINER_NAMES.join(', ')}`);
37
+ }
38
+
39
+ // Create unique key for each container
40
+ const key = `azure-${finalContainerName}`;
21
41
  if (!this.providers.has(key)) {
22
42
  const provider = new AzureStorageProvider(
23
43
  process.env.AZURE_STORAGE_CONNECTION_STRING,
24
- process.env.AZURE_STORAGE_CONTAINER_NAME || "whispertempfiles",
44
+ finalContainerName,
25
45
  );
26
46
  this.providers.set(key, provider);
27
47
  }
@@ -24,6 +24,15 @@ export class StorageProvider {
24
24
  throw new Error("Method not implemented");
25
25
  }
26
26
 
27
+ /**
28
+ * Delete a single file by its URL
29
+ * @param {string} url - The URL of the file to delete
30
+ * @returns {Promise<string|null>} The deleted file path/name or null if not found
31
+ */
32
+ async deleteFile(url) {
33
+ throw new Error("Method not implemented");
34
+ }
35
+
27
36
  /**
28
37
  * Check if a file exists at the given URL
29
38
  * @param {string} url - The URL to check