@intlayer/backend 8.10.0-canary.0 → 8.10.0-canary.1

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 (30) hide show
  1. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/dictionary/markdown.json +1 -8945
  2. package/dist/esm/controllers/ai.controller.mjs +2 -2
  3. package/dist/esm/controllers/ai.controller.mjs.map +1 -1
  4. package/dist/esm/services/audit/recursiveAudit.service.mjs +1 -1
  5. package/dist/esm/services/audit/recursiveAudit.service.mjs.map +1 -1
  6. package/dist/esm/services/cliSessionToken.service.mjs +2 -2
  7. package/dist/esm/services/cliSessionToken.service.mjs.map +1 -1
  8. package/dist/esm/services/dictionary.service.mjs +1 -1
  9. package/dist/esm/services/dictionary.service.mjs.map +1 -1
  10. package/dist/esm/services/oAuth2.service.mjs +3 -3
  11. package/dist/esm/services/oAuth2.service.mjs.map +1 -1
  12. package/dist/esm/services/projectAccessKey.service.mjs +2 -2
  13. package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
  14. package/dist/esm/services/showcase/showcaseProject.service.mjs +1 -1
  15. package/dist/esm/services/showcase/showcaseProject.service.mjs.map +1 -1
  16. package/dist/esm/services/user.service.mjs +1 -1
  17. package/dist/esm/services/user.service.mjs.map +1 -1
  18. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/dictionary/markdown.json +1 -8945
  19. package/dist/types/schemas/dictionary.schema.d.ts +9 -9
  20. package/dist/types/schemas/discussion.schema.d.ts +7 -7
  21. package/dist/types/schemas/organization.schema.d.ts +8 -8
  22. package/dist/types/schemas/plans.schema.d.ts +4 -4
  23. package/dist/types/schemas/project.schema.d.ts +11 -11
  24. package/dist/types/schemas/showcaseProject.schema.d.ts +18 -18
  25. package/dist/types/schemas/tag.schema.d.ts +9 -9
  26. package/dist/types/schemas/user.schema.d.ts +8 -8
  27. package/dist/types/services/showcase/showcaseProject.service.d.ts.map +1 -1
  28. package/dist/types/utils/errors/ErrorHandler.d.ts +4 -4
  29. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +4 -4
  30. package/package.json +14 -14
@@ -267,7 +267,7 @@ const askDocQuestion = async (request, reply) => {
267
267
  if (user?.id) updatePayload.userId = user.id;
268
268
  if (project?.id) updatePayload.projectId = project.id;
269
269
  if (organization?.id) updatePayload.organizationId = organization.id;
270
- await DiscussionModel.findOneAndUpdate({ discussionId }, { $set: updatePayload }, {
270
+ await DiscussionModel.findOneAndUpdate({ discussionId: String(discussionId) }, { $set: updatePayload }, {
271
271
  upsert: true,
272
272
  returnDocument: "after"
273
273
  });
@@ -363,7 +363,7 @@ const chat = async (request, reply) => {
363
363
  if (user?.id) updatePayload.userId = user.id;
364
364
  if (project?.id) updatePayload.projectId = project.id;
365
365
  if (organization?.id) updatePayload.organizationId = organization.id;
366
- await DiscussionModel.findOneAndUpdate({ discussionId }, { $set: updatePayload }, {
366
+ await DiscussionModel.findOneAndUpdate({ discussionId: String(discussionId) }, { $set: updatePayload }, {
367
367
  upsert: true,
368
368
  returnDocument: "after"
369
369
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ai.controller.mjs","names":["customQueryUtil.aiDefaultOptions","customQueryUtil.customQuery","translateJSONUtil.aiDefaultOptions","translateJSONUtil.translateJSON","auditContentDeclarationUtil.aiDefaultOptions","auditContentDeclarationUtil.auditDictionary","auditContentDeclarationFieldUtil.aiDefaultOptions","auditContentDeclarationFieldUtil.auditDictionaryField","auditContentDeclarationMetadataUtil.aiDefaultOptions","tagService.findTags","auditContentDeclarationMetadataUtil.auditDictionaryMetadata","auditTagUtil.aiDefaultOptions","auditTagUtil.auditTag","askDocQuestionUtil.askDocQuestion","chatUtil.chat","autocompleteUtil.aiDefaultOptions","autocompleteUtil.autocomplete"],"sources":["../../../src/controllers/ai.controller.ts"],"sourcesContent":["import {\n type AIConfig,\n type AIOptions,\n type ChatCompletionRequestMessage,\n getAIConfig,\n} from '@intlayer/ai';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { KeyPath } from '@intlayer/types/keyPath';\nimport { logger } from '@logger';\nimport { getDictionariesByTags } from '@services/dictionary.service';\nimport * as tagService from '@services/tag.service';\nimport { getTagsByKeys } from '@services/tag.service';\nimport * as askDocQuestionUtil from '@utils/AI/askDocQuestion/askDocQuestion';\nimport * as auditContentDeclarationUtil from '@utils/AI/auditDictionary';\nimport * as auditContentDeclarationFieldUtil from '@utils/AI/auditDictionaryField';\nimport * as auditContentDeclarationMetadataUtil from '@utils/AI/auditDictionaryMetadata';\nimport * as auditTagUtil from '@utils/AI/auditTag';\nimport * as autocompleteUtil from '@utils/AI/autocomplete';\nimport * as chatUtil from '@utils/AI/chat';\nimport { createSessionTools } from '@utils/AI/chat/sessionTools';\nimport * as customQueryUtil from '@utils/AI/customQuery';\nimport * as translateJSONUtil from '@utils/AI/translateJSON';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport {\n type DiscussionFiltersParams,\n getDiscussionFiltersAndPagination,\n} from '@utils/filtersAndPagination/getDiscussionFiltersAndPagination';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { DiscussionModel } from '@/models/discussion.model';\nimport type { Dictionary } from '@/types/dictionary.types';\nimport type { DiscussionAPI } from '@/types/discussion.types';\nimport type { Tag, TagAPI } from '@/types/tag.types';\n\nexport type {\n AIConfig,\n AIOptions,\n AIProvider,\n ChatCompletionRequestMessage,\n} from '@intlayer/ai';\n\ntype ReplaceAIConfigByOptions<T> = Omit<T, 'aiConfig'> & {\n aiOptions?: AIOptions;\n};\n\nexport type CustomQueryBody =\n ReplaceAIConfigByOptions<customQueryUtil.CustomQueryOptions> & {\n tagsKeys?: string[];\n applicationContext?: string;\n };\nexport type CustomQueryResult =\n ResponseData<customQueryUtil.CustomQueryResultData>;\n\nexport const customQuery = async (\n request: FastifyRequest<{ Body: CustomQueryBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { aiOptions, tagsKeys, ...rest } = request.body;\n const { user, project } = request.session || {};\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: customQueryUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n const auditResponse = await customQueryUtil.customQuery({\n ...rest,\n aiConfig,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'QUERY_FAILED');\n }\n\n const responseData = formatResponse<customQueryUtil.CustomQueryResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type TranslateJSONBody = Omit<\n ReplaceAIConfigByOptions<translateJSONUtil.TranslateJSONOptions<JSON>>,\n 'tags'\n> & {\n tagsKeys?: string[];\n};\nexport type TranslateJSONResult = ResponseData<\n translateJSONUtil.TranslateJSONResultData<JSON>\n>;\n\nexport const translateJSON = async (\n request: FastifyRequest<{ Body: TranslateJSONBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { aiOptions, tagsKeys, ...rest } = request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: translateJSONUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let tags: Tag[] = [];\n\n if (project?.organizationId && tagsKeys) {\n tags = await getTagsByKeys(tagsKeys, project.organizationId);\n }\n\n const auditResponse = await translateJSONUtil.translateJSON<any>({\n ...rest,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n tags,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData = formatResponse<\n translateJSONUtil.TranslateJSONResultData<any>\n >({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditContentDeclarationBody = {\n aiOptions?: AIOptions;\n locales: Locale[];\n defaultLocale: Locale;\n fileContent: string;\n filePath?: string;\n tagsKeys?: string[];\n};\nexport type AuditContentDeclarationResult =\n ResponseData<auditContentDeclarationUtil.AuditFileResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditContentDeclaration = async (\n request: FastifyRequest<{ Body: AuditContentDeclarationBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { fileContent, filePath, aiOptions, locales, defaultLocale, tagsKeys } =\n request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: auditContentDeclarationUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let tags: Tag[] = [];\n\n if (project?.organizationId) {\n tags = await getTagsByKeys(tagsKeys ?? [], project.organizationId);\n }\n\n const auditResponse = await auditContentDeclarationUtil.auditDictionary({\n fileContent,\n filePath,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n locales,\n defaultLocale,\n tags,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData =\n formatResponse<auditContentDeclarationUtil.AuditFileResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditContentDeclarationFieldBody = {\n aiOptions?: AIOptions;\n locales: Locale[];\n fileContent: string;\n filePath?: string;\n tagsKeys?: string[];\n keyPath: KeyPath[];\n};\nexport type AuditContentDeclarationFieldResult =\n ResponseData<auditContentDeclarationFieldUtil.AuditDictionaryFieldResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditContentDeclarationField = async (\n request: FastifyRequest<{ Body: AuditContentDeclarationFieldBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { fileContent, aiOptions, locales, tagsKeys, keyPath } = request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: auditContentDeclarationFieldUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let tags: Tag[] = [];\n\n if (project?.organizationId) {\n tags = await getTagsByKeys(tagsKeys ?? [], project.organizationId);\n }\n\n const auditResponse =\n await auditContentDeclarationFieldUtil.auditDictionaryField({\n fileContent,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n locales,\n tags,\n keyPath,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData =\n formatResponse<auditContentDeclarationFieldUtil.AuditDictionaryFieldResultData>(\n {\n data: auditResponse,\n }\n );\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditContentDeclarationMetadataBody = {\n aiOptions?: AIOptions;\n fileContent: string;\n};\n\nexport type AuditContentDeclarationMetadataResult =\n ResponseData<auditContentDeclarationMetadataUtil.AuditFileResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditContentDeclarationMetadata = async (\n request: FastifyRequest<{ Body: AuditContentDeclarationMetadataBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { organization, user } = request.session || {};\n const { fileContent, aiOptions } = request.body;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n defaultOptions: auditContentDeclarationMetadataUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n const tags: Tag[] = await tagService.findTags(\n {\n organizationId: organization?.id,\n },\n 0,\n 1000\n );\n\n const auditResponse =\n await auditContentDeclarationMetadataUtil.auditDictionaryMetadata({\n fileContent,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n tags,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData =\n formatResponse<auditContentDeclarationMetadataUtil.AuditFileResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditTagBody = {\n aiOptions?: AIOptions;\n tag: TagAPI;\n};\nexport type AuditTagResult = ResponseData<auditTagUtil.TranslateJSONResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditTag = async (\n request: FastifyRequest<{ Body: AuditTagBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { aiOptions, tag } = request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: auditTagUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let dictionaries: Dictionary[] = [];\n if (project?.organizationId) {\n dictionaries = await getDictionariesByTags([tag.key], project.id);\n }\n\n const auditResponse = await auditTagUtil.auditTag({\n aiConfig,\n dictionaries,\n tag,\n applicationContext: aiOptions?.applicationContext,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData = formatResponse<auditTagUtil.TranslateJSONResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AskDocQuestionBody = {\n messages: ChatCompletionRequestMessage[];\n discussionId: string;\n};\nexport type AskDocQuestionResult =\n ResponseData<askDocQuestionUtil.AskDocQuestionResult>;\n\nexport const askDocQuestion = async (\n request: FastifyRequest<{ Body: AskDocQuestionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { messages = [], discussionId } = request.body;\n const { user, project, organization } = request.session || {};\n\n // Hijack response\n reply.hijack();\n\n // Copy all Fastify-managed headers (including CORS) to the raw response\n // immediately after hijacking, before any early returns.\n const headers = reply.getHeaders();\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n }\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n\n // Wrap EVERYTHING in a main try/catch block\n try {\n // Auth Check\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: {},\n projectOptions: projectAIOptions,\n accessType: ['public'],\n },\n !!user\n );\n } catch (error) {\n console.error(error);\n\n // Manually handle this specific error case\n const errorPayload = {\n code: 'AI_ACCESS_DENIED',\n title: 'Access Denied',\n message: 'Unable to configure AI access.',\n };\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n\n reply.raw.end();\n return;\n }\n\n // Set Stream Headers & Flush\n reply.raw.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform');\n reply.raw.setHeader('Connection', 'keep-alive');\n reply.raw.setHeader('X-Accel-Buffering', 'no');\n\n if (reply.raw.flushHeaders) {\n reply.raw.flushHeaders();\n }\n\n reply.raw.write(': connected\\n\\n');\n\n // Execute AI Logic (Awaited properly)\n // This is where 'generateEmbedding' or 'streamText' will throw\n const fullResponse = await askDocQuestionUtil.askDocQuestion(\n messages,\n aiConfig,\n {\n onMessage: (chunk) => {\n if (!reply.raw.writableEnded) {\n reply.raw.write(`data: ${JSON.stringify({ chunk })}\\n\\n`);\n }\n },\n }\n );\n\n // Persist Discussion (Only on success)\n const reversedMessages = [...messages].reverse();\n const lastUserMessageContent = reversedMessages.find(\n (message) => message.role === 'user'\n )?.content;\n const lastUserMessageNbWords =\n typeof lastUserMessageContent === 'string'\n ? lastUserMessageContent.split(' ').length\n : 0;\n\n if (lastUserMessageNbWords >= 2 || messages.length >= 2) {\n const updatePayload: any = {\n discussionId,\n type: 'doc',\n messages: [\n ...messages.map((msg) => ({\n role: msg.role,\n content: msg.content,\n timestamp: msg.timestamp ?? new Date(),\n })),\n {\n role: 'assistant',\n content: fullResponse.response,\n relatedFiles: fullResponse.relatedFiles,\n timestamp: new Date(),\n },\n ],\n };\n\n if (user?.id) updatePayload.userId = user.id;\n if (project?.id) updatePayload.projectId = project.id;\n if (organization?.id) updatePayload.organizationId = organization.id;\n\n await DiscussionModel.findOneAndUpdate(\n { discussionId },\n { $set: updatePayload },\n { upsert: true, returnDocument: 'after' }\n );\n }\n\n // Send Completion Event\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `data: ${JSON.stringify({ done: true, response: fullResponse })}\\n\\n`\n );\n reply.raw.end();\n }\n } catch (err) {\n // -------------------------------------------------------------------------\n // CENTRALIZED ERROR CATCHER\n // -------------------------------------------------------------------------\n const errorMessage = err instanceof Error ? err.message : String(err);\n const errorStack = err instanceof Error ? err.stack : undefined;\n\n // Log the full error to your backend console\n logger.error('AI Stream Error Caught:', {\n message: errorMessage,\n stack: errorStack,\n });\n\n // Determine if it's an Auth error (common with OpenAI 401)\n const isAuthError =\n errorMessage.includes('401') ||\n errorMessage.includes('Incorrect API key');\n\n // Format error for Frontend\n const errorPayload = {\n code: isAuthError ? 'AI_AUTH_ERROR' : 'AI_STREAM_ERROR',\n title: isAuthError ? 'AI Configuration Error' : 'Generation Failed',\n message: errorMessage,\n };\n\n // Send error event to client\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n\nexport type ChatBody = {\n messages: ChatCompletionRequestMessage[];\n discussionId: string;\n};\nexport type ChatResult = ResponseData<chatUtil.ChatResultData>;\n\nexport const chat = async (\n request: FastifyRequest<{ Body: ChatBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { messages = [], discussionId } = request.body;\n const { user, project, organization, roles } = request.session || {};\n\n reply.hijack();\n\n const headers = reply.getHeaders();\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n }\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n try {\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: {},\n projectOptions: projectAIOptions,\n accessType: ['registered_user'],\n },\n !!user\n );\n } catch (error) {\n console.error(error);\n\n const errorPayload = {\n code: 'AI_ACCESS_DENIED',\n title: 'Access Denied',\n message: 'Unable to configure AI access.',\n };\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n reply.raw.end();\n return;\n }\n\n reply.raw.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform');\n reply.raw.setHeader('Connection', 'keep-alive');\n reply.raw.setHeader('X-Accel-Buffering', 'no');\n\n if (reply.raw.flushHeaders) {\n reply.raw.flushHeaders();\n }\n\n reply.raw.write(': connected\\n\\n');\n\n const sessionTools = createSessionTools({\n projectId: project?.id ? String(project.id) : undefined,\n organizationId: organization?.id ? String(organization.id) : undefined,\n userId: user?.id ? String(user.id) : undefined,\n roles: roles || [],\n session: request.session,\n onAction: (action) => {\n if (!reply.raw.writableEnded) {\n reply.raw.write(`data: ${JSON.stringify({ action })}\\n\\n`);\n }\n },\n });\n\n const fullResponse = await chatUtil.chat(messages, aiConfig, {\n tools: sessionTools,\n onMessage: (chunk) => {\n if (!reply.raw.writableEnded) {\n reply.raw.write(`data: ${JSON.stringify({ chunk })}\\n\\n`);\n }\n },\n });\n\n const reversedMessages = [...messages].reverse();\n const lastUserMessageContent = reversedMessages.find(\n (message) => message.role === 'user'\n )?.content;\n const lastUserMessageNbWords =\n typeof lastUserMessageContent === 'string'\n ? lastUserMessageContent.split(' ').length\n : 0;\n\n if (lastUserMessageNbWords >= 2 || messages.length >= 2) {\n const updatePayload: any = {\n discussionId,\n type: 'dashboard',\n messages: [\n ...messages.map((msg) => ({\n role: msg.role,\n content: msg.content,\n timestamp: msg.timestamp ?? new Date(),\n })),\n {\n role: 'assistant',\n content: fullResponse.response,\n timestamp: new Date(),\n },\n ],\n };\n\n if (user?.id) updatePayload.userId = user.id;\n if (project?.id) updatePayload.projectId = project.id;\n if (organization?.id) updatePayload.organizationId = organization.id;\n\n await DiscussionModel.findOneAndUpdate(\n { discussionId },\n { $set: updatePayload },\n { upsert: true, returnDocument: 'after' }\n );\n }\n\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `data: ${JSON.stringify({ done: true, response: fullResponse })}\\n\\n`\n );\n reply.raw.end();\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n const errorStack = err instanceof Error ? err.stack : undefined;\n\n logger.error('AI Chat Stream Error:', {\n message: errorMessage,\n stack: errorStack,\n });\n\n const isAuthError =\n errorMessage.includes('401') ||\n errorMessage.includes('Incorrect API key');\n\n const errorPayload = {\n code: isAuthError ? 'AI_AUTH_ERROR' : 'AI_STREAM_ERROR',\n title: isAuthError ? 'AI Configuration Error' : 'Generation Failed',\n message: errorMessage,\n };\n\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n\nexport type AutocompleteBody = {\n text: string;\n aiOptions?: AIOptions;\n contextBefore?: string;\n currentLine?: string;\n contextAfter?: string;\n};\n\nexport type AutocompleteResponse = ResponseData<{\n autocompletion: string;\n}>;\n\nexport const autocomplete = async (\n request: FastifyRequest<{ Body: AutocompleteBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, project } = request.session || {};\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n try {\n const { text, aiOptions, contextBefore, currentLine, contextAfter } =\n request.body;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: autocompleteUtil.aiDefaultOptions,\n accessType: ['public'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n const response = (await autocompleteUtil.autocomplete({\n text,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n contextBefore,\n currentLine,\n contextAfter,\n })) ?? {\n autocompletion: '',\n tokenUsed: 0,\n };\n\n const responseData =\n formatResponse<autocompleteUtil.AutocompleteFileResultData>({\n data: response,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetDiscussionsParams =\n | ({\n page?: string | number;\n pageSize?: string | number;\n includeMessages?: 'true' | 'false';\n } & DiscussionFiltersParams)\n | undefined;\n\nexport type GetDiscussionsResult = PaginatedResponse<DiscussionAPI>;\n\n/**\n * Retrieves a list of discussions with filters and pagination.\n * Only the owner or admins can access. By default, users only see their own.\n */\nexport const getDiscussions = async (\n request: FastifyRequest<{ Querystring: GetDiscussionsParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, roles } = request.session || {};\n const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } =\n getDiscussionFiltersAndPagination(request);\n const includeMessagesParam = (request.query as any)?.includeMessages as\n | 'true'\n | 'false'\n | undefined;\n const includeMessages = includeMessagesParam !== 'false';\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const projection = includeMessages ? {} : { messages: 0 };\n const discussions = await DiscussionModel.find(filters, projection)\n .sort(sortOptions)\n .skip(skip)\n .limit(pageSize)\n .lean();\n\n // Compute number of messages for each discussion\n const numberOfMessagesById: Record<string, number> = {};\n if (!includeMessages && discussions.length > 0) {\n const ids = discussions.map((d: any) => d._id);\n const counts = await DiscussionModel.aggregate([\n { $match: { _id: { $in: ids } } },\n {\n $project: {\n numberOfMessages: { $size: { $ifNull: ['$messages', []] } },\n },\n },\n ]);\n for (const c of counts as any[]) {\n numberOfMessagesById[String(c._id)] = c.numberOfMessages ?? 0;\n }\n }\n\n // Permission: allow admin, or the owner for all returned entries\n const allOwnedByUser = discussions.every(\n (d) => String(d.userId) === String(user.id)\n );\n const isAllowed = roles?.includes('admin') || allOwnedByUser;\n\n if (!isAllowed) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const totalItems = await DiscussionModel.countDocuments(filters);\n\n const responseData = formatPaginatedResponse({\n data: discussions.map((d: any) => ({\n ...d,\n id: String(d._id ?? d.id),\n numberOfMessages: includeMessages\n ? Array.isArray(d.messages)\n ? d.messages.length\n : 0\n : (numberOfMessagesById[String(d._id ?? d.id)] ?? 0),\n })),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n });\n\n return reply.send(responseData as any);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai.controller.mjs","names":["customQueryUtil.aiDefaultOptions","customQueryUtil.customQuery","translateJSONUtil.aiDefaultOptions","translateJSONUtil.translateJSON","auditContentDeclarationUtil.aiDefaultOptions","auditContentDeclarationUtil.auditDictionary","auditContentDeclarationFieldUtil.aiDefaultOptions","auditContentDeclarationFieldUtil.auditDictionaryField","auditContentDeclarationMetadataUtil.aiDefaultOptions","tagService.findTags","auditContentDeclarationMetadataUtil.auditDictionaryMetadata","auditTagUtil.aiDefaultOptions","auditTagUtil.auditTag","askDocQuestionUtil.askDocQuestion","chatUtil.chat","autocompleteUtil.aiDefaultOptions","autocompleteUtil.autocomplete"],"sources":["../../../src/controllers/ai.controller.ts"],"sourcesContent":["import {\n type AIConfig,\n type AIOptions,\n type ChatCompletionRequestMessage,\n getAIConfig,\n} from '@intlayer/ai';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { KeyPath } from '@intlayer/types/keyPath';\nimport { logger } from '@logger';\nimport { getDictionariesByTags } from '@services/dictionary.service';\nimport * as tagService from '@services/tag.service';\nimport { getTagsByKeys } from '@services/tag.service';\nimport * as askDocQuestionUtil from '@utils/AI/askDocQuestion/askDocQuestion';\nimport * as auditContentDeclarationUtil from '@utils/AI/auditDictionary';\nimport * as auditContentDeclarationFieldUtil from '@utils/AI/auditDictionaryField';\nimport * as auditContentDeclarationMetadataUtil from '@utils/AI/auditDictionaryMetadata';\nimport * as auditTagUtil from '@utils/AI/auditTag';\nimport * as autocompleteUtil from '@utils/AI/autocomplete';\nimport * as chatUtil from '@utils/AI/chat';\nimport { createSessionTools } from '@utils/AI/chat/sessionTools';\nimport * as customQueryUtil from '@utils/AI/customQuery';\nimport * as translateJSONUtil from '@utils/AI/translateJSON';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport {\n type DiscussionFiltersParams,\n getDiscussionFiltersAndPagination,\n} from '@utils/filtersAndPagination/getDiscussionFiltersAndPagination';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { DiscussionModel } from '@/models/discussion.model';\nimport type { Dictionary } from '@/types/dictionary.types';\nimport type { DiscussionAPI } from '@/types/discussion.types';\nimport type { Tag, TagAPI } from '@/types/tag.types';\n\nexport type {\n AIConfig,\n AIOptions,\n AIProvider,\n ChatCompletionRequestMessage,\n} from '@intlayer/ai';\n\ntype ReplaceAIConfigByOptions<T> = Omit<T, 'aiConfig'> & {\n aiOptions?: AIOptions;\n};\n\nexport type CustomQueryBody =\n ReplaceAIConfigByOptions<customQueryUtil.CustomQueryOptions> & {\n tagsKeys?: string[];\n applicationContext?: string;\n };\nexport type CustomQueryResult =\n ResponseData<customQueryUtil.CustomQueryResultData>;\n\nexport const customQuery = async (\n request: FastifyRequest<{ Body: CustomQueryBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { aiOptions, tagsKeys, ...rest } = request.body;\n const { user, project } = request.session || {};\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: customQueryUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n const auditResponse = await customQueryUtil.customQuery({\n ...rest,\n aiConfig,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'QUERY_FAILED');\n }\n\n const responseData = formatResponse<customQueryUtil.CustomQueryResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type TranslateJSONBody = Omit<\n ReplaceAIConfigByOptions<translateJSONUtil.TranslateJSONOptions<JSON>>,\n 'tags'\n> & {\n tagsKeys?: string[];\n};\nexport type TranslateJSONResult = ResponseData<\n translateJSONUtil.TranslateJSONResultData<JSON>\n>;\n\nexport const translateJSON = async (\n request: FastifyRequest<{ Body: TranslateJSONBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { aiOptions, tagsKeys, ...rest } = request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: translateJSONUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let tags: Tag[] = [];\n\n if (project?.organizationId && tagsKeys) {\n tags = await getTagsByKeys(tagsKeys, project.organizationId);\n }\n\n const auditResponse = await translateJSONUtil.translateJSON<any>({\n ...rest,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n tags,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData = formatResponse<\n translateJSONUtil.TranslateJSONResultData<any>\n >({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditContentDeclarationBody = {\n aiOptions?: AIOptions;\n locales: Locale[];\n defaultLocale: Locale;\n fileContent: string;\n filePath?: string;\n tagsKeys?: string[];\n};\nexport type AuditContentDeclarationResult =\n ResponseData<auditContentDeclarationUtil.AuditFileResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditContentDeclaration = async (\n request: FastifyRequest<{ Body: AuditContentDeclarationBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { fileContent, filePath, aiOptions, locales, defaultLocale, tagsKeys } =\n request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: auditContentDeclarationUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let tags: Tag[] = [];\n\n if (project?.organizationId) {\n tags = await getTagsByKeys(tagsKeys ?? [], project.organizationId);\n }\n\n const auditResponse = await auditContentDeclarationUtil.auditDictionary({\n fileContent,\n filePath,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n locales,\n defaultLocale,\n tags,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData =\n formatResponse<auditContentDeclarationUtil.AuditFileResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditContentDeclarationFieldBody = {\n aiOptions?: AIOptions;\n locales: Locale[];\n fileContent: string;\n filePath?: string;\n tagsKeys?: string[];\n keyPath: KeyPath[];\n};\nexport type AuditContentDeclarationFieldResult =\n ResponseData<auditContentDeclarationFieldUtil.AuditDictionaryFieldResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditContentDeclarationField = async (\n request: FastifyRequest<{ Body: AuditContentDeclarationFieldBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { fileContent, aiOptions, locales, tagsKeys, keyPath } = request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: auditContentDeclarationFieldUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let tags: Tag[] = [];\n\n if (project?.organizationId) {\n tags = await getTagsByKeys(tagsKeys ?? [], project.organizationId);\n }\n\n const auditResponse =\n await auditContentDeclarationFieldUtil.auditDictionaryField({\n fileContent,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n locales,\n tags,\n keyPath,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData =\n formatResponse<auditContentDeclarationFieldUtil.AuditDictionaryFieldResultData>(\n {\n data: auditResponse,\n }\n );\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditContentDeclarationMetadataBody = {\n aiOptions?: AIOptions;\n fileContent: string;\n};\n\nexport type AuditContentDeclarationMetadataResult =\n ResponseData<auditContentDeclarationMetadataUtil.AuditFileResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditContentDeclarationMetadata = async (\n request: FastifyRequest<{ Body: AuditContentDeclarationMetadataBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { organization, user } = request.session || {};\n const { fileContent, aiOptions } = request.body;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n defaultOptions: auditContentDeclarationMetadataUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n const tags: Tag[] = await tagService.findTags(\n {\n organizationId: organization?.id,\n },\n 0,\n 1000\n );\n\n const auditResponse =\n await auditContentDeclarationMetadataUtil.auditDictionaryMetadata({\n fileContent,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n tags,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData =\n formatResponse<auditContentDeclarationMetadataUtil.AuditFileResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AuditTagBody = {\n aiOptions?: AIOptions;\n tag: TagAPI;\n};\nexport type AuditTagResult = ResponseData<auditTagUtil.TranslateJSONResultData>;\n\n/**\n * Retrieves a list of dictionaries based on filters and pagination.\n */\nexport const auditTag = async (\n request: FastifyRequest<{ Body: AuditTagBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { aiOptions, tag } = request.body;\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: auditTagUtil.aiDefaultOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n try {\n let dictionaries: Dictionary[] = [];\n if (project?.organizationId) {\n dictionaries = await getDictionariesByTags([tag.key], project.id);\n }\n\n const auditResponse = await auditTagUtil.auditTag({\n aiConfig,\n dictionaries,\n tag,\n applicationContext: aiOptions?.applicationContext,\n });\n\n if (!auditResponse) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AUDIT_FAILED');\n }\n\n const responseData = formatResponse<auditTagUtil.TranslateJSONResultData>({\n data: auditResponse,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AskDocQuestionBody = {\n messages: ChatCompletionRequestMessage[];\n discussionId: string;\n};\nexport type AskDocQuestionResult =\n ResponseData<askDocQuestionUtil.AskDocQuestionResult>;\n\nexport const askDocQuestion = async (\n request: FastifyRequest<{ Body: AskDocQuestionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { messages = [], discussionId } = request.body;\n const { user, project, organization } = request.session || {};\n\n // Hijack response\n reply.hijack();\n\n // Copy all Fastify-managed headers (including CORS) to the raw response\n // immediately after hijacking, before any early returns.\n const headers = reply.getHeaders();\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n }\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n let aiConfig: AIConfig;\n\n // Wrap EVERYTHING in a main try/catch block\n try {\n // Auth Check\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: {},\n projectOptions: projectAIOptions,\n accessType: ['public'],\n },\n !!user\n );\n } catch (error) {\n console.error(error);\n\n // Manually handle this specific error case\n const errorPayload = {\n code: 'AI_ACCESS_DENIED',\n title: 'Access Denied',\n message: 'Unable to configure AI access.',\n };\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n\n reply.raw.end();\n return;\n }\n\n // Set Stream Headers & Flush\n reply.raw.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform');\n reply.raw.setHeader('Connection', 'keep-alive');\n reply.raw.setHeader('X-Accel-Buffering', 'no');\n\n if (reply.raw.flushHeaders) {\n reply.raw.flushHeaders();\n }\n\n reply.raw.write(': connected\\n\\n');\n\n // Execute AI Logic (Awaited properly)\n // This is where 'generateEmbedding' or 'streamText' will throw\n const fullResponse = await askDocQuestionUtil.askDocQuestion(\n messages,\n aiConfig,\n {\n onMessage: (chunk) => {\n if (!reply.raw.writableEnded) {\n reply.raw.write(`data: ${JSON.stringify({ chunk })}\\n\\n`);\n }\n },\n }\n );\n\n // Persist Discussion (Only on success)\n const reversedMessages = [...messages].reverse();\n const lastUserMessageContent = reversedMessages.find(\n (message) => message.role === 'user'\n )?.content;\n const lastUserMessageNbWords =\n typeof lastUserMessageContent === 'string'\n ? lastUserMessageContent.split(' ').length\n : 0;\n\n if (lastUserMessageNbWords >= 2 || messages.length >= 2) {\n const updatePayload: any = {\n discussionId,\n type: 'doc',\n messages: [\n ...messages.map((msg) => ({\n role: msg.role,\n content: msg.content,\n timestamp: msg.timestamp ?? new Date(),\n })),\n {\n role: 'assistant',\n content: fullResponse.response,\n relatedFiles: fullResponse.relatedFiles,\n timestamp: new Date(),\n },\n ],\n };\n\n if (user?.id) updatePayload.userId = user.id;\n if (project?.id) updatePayload.projectId = project.id;\n if (organization?.id) updatePayload.organizationId = organization.id;\n\n await DiscussionModel.findOneAndUpdate(\n { discussionId: String(discussionId) },\n { $set: updatePayload },\n { upsert: true, returnDocument: 'after' }\n );\n }\n\n // Send Completion Event\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `data: ${JSON.stringify({ done: true, response: fullResponse })}\\n\\n`\n );\n reply.raw.end();\n }\n } catch (err) {\n // -------------------------------------------------------------------------\n // CENTRALIZED ERROR CATCHER\n // -------------------------------------------------------------------------\n const errorMessage = err instanceof Error ? err.message : String(err);\n const errorStack = err instanceof Error ? err.stack : undefined;\n\n // Log the full error to your backend console\n logger.error('AI Stream Error Caught:', {\n message: errorMessage,\n stack: errorStack,\n });\n\n // Determine if it's an Auth error (common with OpenAI 401)\n const isAuthError =\n errorMessage.includes('401') ||\n errorMessage.includes('Incorrect API key');\n\n // Format error for Frontend\n const errorPayload = {\n code: isAuthError ? 'AI_AUTH_ERROR' : 'AI_STREAM_ERROR',\n title: isAuthError ? 'AI Configuration Error' : 'Generation Failed',\n message: errorMessage,\n };\n\n // Send error event to client\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n\nexport type ChatBody = {\n messages: ChatCompletionRequestMessage[];\n discussionId: string;\n};\nexport type ChatResult = ResponseData<chatUtil.ChatResultData>;\n\nexport const chat = async (\n request: FastifyRequest<{ Body: ChatBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { messages = [], discussionId } = request.body;\n const { user, project, organization, roles } = request.session || {};\n\n reply.hijack();\n\n const headers = reply.getHeaders();\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n }\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n try {\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: {},\n projectOptions: projectAIOptions,\n accessType: ['registered_user'],\n },\n !!user\n );\n } catch (error) {\n console.error(error);\n\n const errorPayload = {\n code: 'AI_ACCESS_DENIED',\n title: 'Access Denied',\n message: 'Unable to configure AI access.',\n };\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n reply.raw.end();\n return;\n }\n\n reply.raw.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform');\n reply.raw.setHeader('Connection', 'keep-alive');\n reply.raw.setHeader('X-Accel-Buffering', 'no');\n\n if (reply.raw.flushHeaders) {\n reply.raw.flushHeaders();\n }\n\n reply.raw.write(': connected\\n\\n');\n\n const sessionTools = createSessionTools({\n projectId: project?.id ? String(project.id) : undefined,\n organizationId: organization?.id ? String(organization.id) : undefined,\n userId: user?.id ? String(user.id) : undefined,\n roles: roles || [],\n session: request.session,\n onAction: (action) => {\n if (!reply.raw.writableEnded) {\n reply.raw.write(`data: ${JSON.stringify({ action })}\\n\\n`);\n }\n },\n });\n\n const fullResponse = await chatUtil.chat(messages, aiConfig, {\n tools: sessionTools,\n onMessage: (chunk) => {\n if (!reply.raw.writableEnded) {\n reply.raw.write(`data: ${JSON.stringify({ chunk })}\\n\\n`);\n }\n },\n });\n\n const reversedMessages = [...messages].reverse();\n const lastUserMessageContent = reversedMessages.find(\n (message) => message.role === 'user'\n )?.content;\n const lastUserMessageNbWords =\n typeof lastUserMessageContent === 'string'\n ? lastUserMessageContent.split(' ').length\n : 0;\n\n if (lastUserMessageNbWords >= 2 || messages.length >= 2) {\n const updatePayload: any = {\n discussionId,\n type: 'dashboard',\n messages: [\n ...messages.map((msg) => ({\n role: msg.role,\n content: msg.content,\n timestamp: msg.timestamp ?? new Date(),\n })),\n {\n role: 'assistant',\n content: fullResponse.response,\n timestamp: new Date(),\n },\n ],\n };\n\n if (user?.id) updatePayload.userId = user.id;\n if (project?.id) updatePayload.projectId = project.id;\n if (organization?.id) updatePayload.organizationId = organization.id;\n\n await DiscussionModel.findOneAndUpdate(\n { discussionId: String(discussionId) },\n { $set: updatePayload },\n { upsert: true, returnDocument: 'after' }\n );\n }\n\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `data: ${JSON.stringify({ done: true, response: fullResponse })}\\n\\n`\n );\n reply.raw.end();\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n const errorStack = err instanceof Error ? err.stack : undefined;\n\n logger.error('AI Chat Stream Error:', {\n message: errorMessage,\n stack: errorStack,\n });\n\n const isAuthError =\n errorMessage.includes('401') ||\n errorMessage.includes('Incorrect API key');\n\n const errorPayload = {\n code: isAuthError ? 'AI_AUTH_ERROR' : 'AI_STREAM_ERROR',\n title: isAuthError ? 'AI Configuration Error' : 'Generation Failed',\n message: errorMessage,\n };\n\n if (!reply.raw.writableEnded) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n\nexport type AutocompleteBody = {\n text: string;\n aiOptions?: AIOptions;\n contextBefore?: string;\n currentLine?: string;\n contextAfter?: string;\n};\n\nexport type AutocompleteResponse = ResponseData<{\n autocompletion: string;\n}>;\n\nexport const autocomplete = async (\n request: FastifyRequest<{ Body: AutocompleteBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, project } = request.session || {};\n\n const projectAIOptions = project?.configuration?.ai\n ? (project.configuration.ai as AIOptions)\n : undefined;\n\n try {\n const { text, aiOptions, contextBefore, currentLine, contextAfter } =\n request.body;\n\n let aiConfig: AIConfig;\n try {\n aiConfig = await getAIConfig(\n {\n userOptions: aiOptions,\n projectOptions: projectAIOptions,\n defaultOptions: autocompleteUtil.aiDefaultOptions,\n accessType: ['public'],\n },\n !!user\n );\n } catch (_error) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'AI_ACCESS_DENIED');\n }\n\n const response = (await autocompleteUtil.autocomplete({\n text,\n aiConfig,\n applicationContext: aiOptions?.applicationContext,\n contextBefore,\n currentLine,\n contextAfter,\n })) ?? {\n autocompletion: '',\n tokenUsed: 0,\n };\n\n const responseData =\n formatResponse<autocompleteUtil.AutocompleteFileResultData>({\n data: response,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetDiscussionsParams =\n | ({\n page?: string | number;\n pageSize?: string | number;\n includeMessages?: 'true' | 'false';\n } & DiscussionFiltersParams)\n | undefined;\n\nexport type GetDiscussionsResult = PaginatedResponse<DiscussionAPI>;\n\n/**\n * Retrieves a list of discussions with filters and pagination.\n * Only the owner or admins can access. By default, users only see their own.\n */\nexport const getDiscussions = async (\n request: FastifyRequest<{ Querystring: GetDiscussionsParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, roles } = request.session || {};\n const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } =\n getDiscussionFiltersAndPagination(request);\n const includeMessagesParam = (request.query as any)?.includeMessages as\n | 'true'\n | 'false'\n | undefined;\n const includeMessages = includeMessagesParam !== 'false';\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const projection = includeMessages ? {} : { messages: 0 };\n const discussions = await DiscussionModel.find(filters, projection)\n .sort(sortOptions)\n .skip(skip)\n .limit(pageSize)\n .lean();\n\n // Compute number of messages for each discussion\n const numberOfMessagesById: Record<string, number> = {};\n if (!includeMessages && discussions.length > 0) {\n const ids = discussions.map((d: any) => d._id);\n const counts = await DiscussionModel.aggregate([\n { $match: { _id: { $in: ids } } },\n {\n $project: {\n numberOfMessages: { $size: { $ifNull: ['$messages', []] } },\n },\n },\n ]);\n for (const c of counts as any[]) {\n numberOfMessagesById[String(c._id)] = c.numberOfMessages ?? 0;\n }\n }\n\n // Permission: allow admin, or the owner for all returned entries\n const allOwnedByUser = discussions.every(\n (d) => String(d.userId) === String(user.id)\n );\n const isAllowed = roles?.includes('admin') || allOwnedByUser;\n\n if (!isAllowed) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const totalItems = await DiscussionModel.countDocuments(filters);\n\n const responseData = formatPaginatedResponse({\n data: discussions.map((d: any) => ({\n ...d,\n id: String(d._id ?? d.id),\n numberOfMessages: includeMessages\n ? Array.isArray(d.messages)\n ? d.messages.length\n : 0\n : (numberOfMessagesById[String(d._id ?? d.id)] ?? 0),\n })),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n });\n\n return reply.send(responseData as any);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0DA,MAAa,cAAc,OACzB,SACA,UACkB;CAClB,MAAM,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CACjD,MAAM,EAAE,MAAM,YAAY,QAAQ,WAAW,CAAC;CAE9C,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YACf;GACE,aAAa;GACb,gBAAgB;GAChB,gBAAgBA;GAChB,YAAY,CAAC,mBAAmB,QAAQ;EAC1C,GACA,CAAC,CAAC,IACJ;CACF,SAAS,QAAQ;EACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAC1E;CAEA,IAAI;EACF,MAAM,gBAAgB,MAAMC,cAA4B;GACtD,GAAG;GACH;EACF,CAAC;EAED,IAAI,CAAC,eACH,OAAO,aAAa,2BAA2B,OAAO,cAAc;EAGtE,MAAM,eAAe,eAAsD,EACzE,MAAM,cACR,CAAC;EAED,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;AAYA,MAAa,gBAAgB,OAC3B,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,SAAS,QAAQ,WAAW,CAAC;CAC9C,MAAM,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAEjD,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YACf;GACE,aAAa;GACb,gBAAgB;GAChB,gBAAgBC;GAChB,YAAY,CAAC,mBAAmB,QAAQ;EAC1C,GACA,CAAC,CAAC,IACJ;CACF,SAAS,QAAQ;EACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAC1E;CAEA,IAAI;EACF,IAAI,OAAc,CAAC;EAEnB,IAAI,SAAS,kBAAkB,UAC7B,OAAO,MAAM,cAAc,UAAU,QAAQ,cAAc;EAG7D,MAAM,gBAAgB,MAAMC,gBAAqC;GAC/D,GAAG;GACH;GACA,oBAAoB,WAAW;GAC/B;EACF,CAAC;EAED,IAAI,CAAC,eACH,OAAO,aAAa,2BAA2B,OAAO,cAAc;EAGtE,MAAM,eAAe,eAEnB,EACA,MAAM,cACR,CAAC;EAED,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;;;;AAgBA,MAAa,0BAA0B,OACrC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,SAAS,QAAQ,WAAW,CAAC;CAC9C,MAAM,EAAE,aAAa,UAAU,WAAW,SAAS,eAAe,aAChE,QAAQ;CAEV,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YACf;GACE,aAAa;GACb,gBAAgB;GAChB,gBAAgBC;GAChB,YAAY,CAAC,mBAAmB,QAAQ;EAC1C,GACA,CAAC,CAAC,IACJ;CACF,SAAS,QAAQ;EACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAC1E;CAEA,IAAI;EACF,IAAI,OAAc,CAAC;EAEnB,IAAI,SAAS,gBACX,OAAO,MAAM,cAAc,YAAY,CAAC,GAAG,QAAQ,cAAc;EAGnE,MAAM,gBAAgB,MAAMC,gBAA4C;GACtE;GACA;GACA;GACA,oBAAoB,WAAW;GAC/B;GACA;GACA;EACF,CAAC;EAED,IAAI,CAAC,eACH,OAAO,aAAa,2BAA2B,OAAO,cAAc;EAGtE,MAAM,eACJ,eAAgE,EAC9D,MAAM,cACR,CAAC;EAEH,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;;;;AAgBA,MAAa,+BAA+B,OAC1C,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,SAAS,QAAQ,WAAW,CAAC;CAC9C,MAAM,EAAE,aAAa,WAAW,SAAS,UAAU,YAAY,QAAQ;CAEvE,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YACf;GACE,aAAa;GACb,gBAAgB;GAChB,gBAAgBC;GAChB,YAAY,CAAC,mBAAmB,QAAQ;EAC1C,GACA,CAAC,CAAC,IACJ;CACF,SAAS,QAAQ;EACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAC1E;CAEA,IAAI;EACF,IAAI,OAAc,CAAC;EAEnB,IAAI,SAAS,gBACX,OAAO,MAAM,cAAc,YAAY,CAAC,GAAG,QAAQ,cAAc;EAGnE,MAAM,gBACJ,MAAMC,qBAAsD;GAC1D;GACA;GACA,oBAAoB,WAAW;GAC/B;GACA;GACA;EACF,CAAC;EAEH,IAAI,CAAC,eACH,OAAO,aAAa,2BAA2B,OAAO,cAAc;EAGtE,MAAM,eACJ,eACE,EACE,MAAM,cACR,CACF;EAEF,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;;;;AAaA,MAAa,kCAAkC,OAC7C,SACA,UACkB;CAClB,MAAM,EAAE,cAAc,SAAS,QAAQ,WAAW,CAAC;CACnD,MAAM,EAAE,aAAa,cAAc,QAAQ;CAE3C,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YACf;GACE,aAAa;GACb,gBAAgBC;GAChB,YAAY,CAAC,mBAAmB,QAAQ;EAC1C,GACA,CAAC,CAAC,IACJ;CACF,SAAS,QAAQ;EACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAC1E;CAEA,IAAI;EACF,MAAM,OAAc,MAAMC,SACxB,EACE,gBAAgB,cAAc,GAChC,GACA,GACA,GACF;EAEA,MAAM,gBACJ,MAAMC,0BAA4D;GAChE;GACA;GACA,oBAAoB,WAAW;GAC/B;EACF,CAAC;EAEH,IAAI,CAAC,eACH,OAAO,aAAa,2BAA2B,OAAO,cAAc;EAGtE,MAAM,eACJ,eAAwE,EACtE,MAAM,cACR,CAAC;EAEH,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;;;;AAWA,MAAa,WAAW,OACtB,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,SAAS,QAAQ,WAAW,CAAC;CAC9C,MAAM,EAAE,WAAW,QAAQ,QAAQ;CAEnC,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YACf;GACE,aAAa;GACb,gBAAgB;GAChB,gBAAgBC;GAChB,YAAY,CAAC,mBAAmB,QAAQ;EAC1C,GACA,CAAC,CAAC,IACJ;CACF,SAAS,QAAQ;EACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAC1E;CAEA,IAAI;EACF,IAAI,eAA6B,CAAC;EAClC,IAAI,SAAS,gBACX,eAAe,MAAM,sBAAsB,CAAC,IAAI,GAAG,GAAG,QAAQ,EAAE;EAGlE,MAAM,gBAAgB,MAAMC,WAAsB;GAChD;GACA;GACA;GACA,oBAAoB,WAAW;EACjC,CAAC;EAED,IAAI,CAAC,eACH,OAAO,aAAa,2BAA2B,OAAO,cAAc;EAGtE,MAAM,eAAe,eAAqD,EACxE,MAAM,cACR,CAAC;EAED,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;AASA,MAAa,iBAAiB,OAC5B,SACA,UACkB;CAClB,MAAM,EAAE,WAAW,CAAC,GAAG,iBAAiB,QAAQ;CAChD,MAAM,EAAE,MAAM,SAAS,iBAAiB,QAAQ,WAAW,CAAC;CAG5D,MAAM,OAAO;CAIb,MAAM,UAAU,MAAM,WAAW;CACjC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAC/C,IAAI,UAAU,QACZ,MAAM,IAAI,UAAU,KAAK,KAAK;CAIlC,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;CAGJ,IAAI;EAEF,IAAI;GACF,WAAW,MAAM,YACf;IACE,aAAa,CAAC;IACd,gBAAgB;IAChB,YAAY,CAAC,QAAQ;GACvB,GACA,CAAC,CAAC,IACJ;EACF,SAAS,OAAO;GACd,QAAQ,MAAM,KAAK;GAQnB,MAAM,IAAI,MACR,uBAAuB,KAAK,UAAU;IALtC,MAAM;IACN,OAAO;IACP,SAAS;GAGwC,CAAC,EAAE,KACtD;GAEA,MAAM,IAAI,IAAI;GACd;EACF;EAGA,MAAM,IAAI,UAAU,gBAAgB,kCAAkC;EACtE,MAAM,IAAI,UAAU,iBAAiB,wBAAwB;EAC7D,MAAM,IAAI,UAAU,cAAc,YAAY;EAC9C,MAAM,IAAI,UAAU,qBAAqB,IAAI;EAE7C,IAAI,MAAM,IAAI,cACZ,MAAM,IAAI,aAAa;EAGzB,MAAM,IAAI,MAAM,iBAAiB;EAIjC,MAAM,eAAe,MAAMC,iBACzB,UACA,UACA,EACE,YAAY,UAAU;GACpB,IAAI,CAAC,MAAM,IAAI,eACb,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,EAAE,MAAM,CAAC,EAAE,KAAK;EAE5D,EACF,CACF;EAIA,MAAM,yBADmB,CAAC,GAAG,QAAQ,EAAE,QACO,EAAE,MAC7C,YAAY,QAAQ,SAAS,MAChC,GAAG;EAMH,KAJE,OAAO,2BAA2B,WAC9B,uBAAuB,MAAM,GAAG,EAAE,SAClC,MAEwB,KAAK,SAAS,UAAU,GAAG;GACvD,MAAM,gBAAqB;IACzB;IACA,MAAM;IACN,UAAU,CACR,GAAG,SAAS,KAAK,SAAS;KACxB,MAAM,IAAI;KACV,SAAS,IAAI;KACb,WAAW,IAAI,6BAAa,IAAI,KAAK;IACvC,EAAE,GACF;KACE,MAAM;KACN,SAAS,aAAa;KACtB,cAAc,aAAa;KAC3B,2BAAW,IAAI,KAAK;IACtB,CACF;GACF;GAEA,IAAI,MAAM,IAAI,cAAc,SAAS,KAAK;GAC1C,IAAI,SAAS,IAAI,cAAc,YAAY,QAAQ;GACnD,IAAI,cAAc,IAAI,cAAc,iBAAiB,aAAa;GAElE,MAAM,gBAAgB,iBACpB,EAAE,cAAc,OAAO,YAAY,EAAE,GACrC,EAAE,MAAM,cAAc,GACtB;IAAE,QAAQ;IAAM,gBAAgB;GAAQ,CAC1C;EACF;EAGA,IAAI,CAAC,MAAM,IAAI,eAAe;GAC5B,MAAM,IAAI,MACR,SAAS,KAAK,UAAU;IAAE,MAAM;IAAM,UAAU;GAAa,CAAC,EAAE,KAClE;GACA,MAAM,IAAI,IAAI;EAChB;CACF,SAAS,KAAK;EAIZ,MAAM,eAAe,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EACpE,MAAM,aAAa,eAAe,QAAQ,IAAI,QAAQ;EAGtD,OAAO,MAAM,2BAA2B;GACtC,SAAS;GACT,OAAO;EACT,CAAC;EAGD,MAAM,cACJ,aAAa,SAAS,KAAK,KAC3B,aAAa,SAAS,mBAAmB;EAG3C,MAAM,eAAe;GACnB,MAAM,cAAc,kBAAkB;GACtC,OAAO,cAAc,2BAA2B;GAChD,SAAS;EACX;EAGA,IAAI,CAAC,MAAM,IAAI,eAAe;GAC5B,MAAM,IAAI,MACR,uBAAuB,KAAK,UAAU,YAAY,EAAE,KACtD;GACA,MAAM,IAAI,IAAI;EAChB;CACF;AACF;AAQA,MAAa,OAAO,OAClB,SACA,UACkB;CAClB,MAAM,EAAE,WAAW,CAAC,GAAG,iBAAiB,QAAQ;CAChD,MAAM,EAAE,MAAM,SAAS,cAAc,UAAU,QAAQ,WAAW,CAAC;CAEnE,MAAM,OAAO;CAEb,MAAM,UAAU,MAAM,WAAW;CACjC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAC/C,IAAI,UAAU,QACZ,MAAM,IAAI,UAAU,KAAK,KAAK;CAIlC,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;EACF,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,YACf;IACE,aAAa,CAAC;IACd,gBAAgB;IAChB,YAAY,CAAC,iBAAiB;GAChC,GACA,CAAC,CAAC,IACJ;EACF,SAAS,OAAO;GACd,QAAQ,MAAM,KAAK;GAOnB,MAAM,IAAI,MACR,uBAAuB,KAAK,UAAU;IALtC,MAAM;IACN,OAAO;IACP,SAAS;GAGwC,CAAC,EAAE,KACtD;GACA,MAAM,IAAI,IAAI;GACd;EACF;EAEA,MAAM,IAAI,UAAU,gBAAgB,kCAAkC;EACtE,MAAM,IAAI,UAAU,iBAAiB,wBAAwB;EAC7D,MAAM,IAAI,UAAU,cAAc,YAAY;EAC9C,MAAM,IAAI,UAAU,qBAAqB,IAAI;EAE7C,IAAI,MAAM,IAAI,cACZ,MAAM,IAAI,aAAa;EAGzB,MAAM,IAAI,MAAM,iBAAiB;EAEjC,MAAM,eAAe,mBAAmB;GACtC,WAAW,SAAS,KAAK,OAAO,QAAQ,EAAE,IAAI;GAC9C,gBAAgB,cAAc,KAAK,OAAO,aAAa,EAAE,IAAI;GAC7D,QAAQ,MAAM,KAAK,OAAO,KAAK,EAAE,IAAI;GACrC,OAAO,SAAS,CAAC;GACjB,SAAS,QAAQ;GACjB,WAAW,WAAW;IACpB,IAAI,CAAC,MAAM,IAAI,eACb,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,KAAK;GAE7D;EACF,CAAC;EAED,MAAM,eAAe,MAAMC,OAAc,UAAU,UAAU;GAC3D,OAAO;GACP,YAAY,UAAU;IACpB,IAAI,CAAC,MAAM,IAAI,eACb,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,EAAE,MAAM,CAAC,EAAE,KAAK;GAE5D;EACF,CAAC;EAGD,MAAM,yBADmB,CAAC,GAAG,QAAQ,EAAE,QACO,EAAE,MAC7C,YAAY,QAAQ,SAAS,MAChC,GAAG;EAMH,KAJE,OAAO,2BAA2B,WAC9B,uBAAuB,MAAM,GAAG,EAAE,SAClC,MAEwB,KAAK,SAAS,UAAU,GAAG;GACvD,MAAM,gBAAqB;IACzB;IACA,MAAM;IACN,UAAU,CACR,GAAG,SAAS,KAAK,SAAS;KACxB,MAAM,IAAI;KACV,SAAS,IAAI;KACb,WAAW,IAAI,6BAAa,IAAI,KAAK;IACvC,EAAE,GACF;KACE,MAAM;KACN,SAAS,aAAa;KACtB,2BAAW,IAAI,KAAK;IACtB,CACF;GACF;GAEA,IAAI,MAAM,IAAI,cAAc,SAAS,KAAK;GAC1C,IAAI,SAAS,IAAI,cAAc,YAAY,QAAQ;GACnD,IAAI,cAAc,IAAI,cAAc,iBAAiB,aAAa;GAElE,MAAM,gBAAgB,iBACpB,EAAE,cAAc,OAAO,YAAY,EAAE,GACrC,EAAE,MAAM,cAAc,GACtB;IAAE,QAAQ;IAAM,gBAAgB;GAAQ,CAC1C;EACF;EAEA,IAAI,CAAC,MAAM,IAAI,eAAe;GAC5B,MAAM,IAAI,MACR,SAAS,KAAK,UAAU;IAAE,MAAM;IAAM,UAAU;GAAa,CAAC,EAAE,KAClE;GACA,MAAM,IAAI,IAAI;EAChB;CACF,SAAS,KAAK;EACZ,MAAM,eAAe,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EACpE,MAAM,aAAa,eAAe,QAAQ,IAAI,QAAQ;EAEtD,OAAO,MAAM,yBAAyB;GACpC,SAAS;GACT,OAAO;EACT,CAAC;EAED,MAAM,cACJ,aAAa,SAAS,KAAK,KAC3B,aAAa,SAAS,mBAAmB;EAE3C,MAAM,eAAe;GACnB,MAAM,cAAc,kBAAkB;GACtC,OAAO,cAAc,2BAA2B;GAChD,SAAS;EACX;EAEA,IAAI,CAAC,MAAM,IAAI,eAAe;GAC5B,MAAM,IAAI,MACR,uBAAuB,KAAK,UAAU,YAAY,EAAE,KACtD;GACA,MAAM,IAAI,IAAI;EAChB;CACF;AACF;AAcA,MAAa,eAAe,OAC1B,SACA,UACkB;CAClB,MAAM,EAAE,MAAM,YAAY,QAAQ,WAAW,CAAC;CAE9C,MAAM,mBAAmB,SAAS,eAAe,KAC5C,QAAQ,cAAc,KACvB;CAEJ,IAAI;EACF,MAAM,EAAE,MAAM,WAAW,eAAe,aAAa,iBACnD,QAAQ;EAEV,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,YACf;IACE,aAAa;IACb,gBAAgB;IAChB,gBAAgBC;IAChB,YAAY,CAAC,QAAQ;GACvB,GACA,CAAC,CAAC,IACJ;EACF,SAAS,QAAQ;GACf,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;EAC1E;EAcA,MAAM,eACJ,eAA4D,EAC1D,MAdc,MAAMC,eAA8B;GACpD;GACA;GACA,oBAAoB,WAAW;GAC/B;GACA;GACA;EACF,CAAC,KAAM;GACL,gBAAgB;GAChB,WAAW;EACb,EAKE,CAAC;EAEH,OAAO,MAAM,KAAK,YAAY;CAChC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF;;;;;AAgBA,MAAa,iBAAiB,OAC5B,SACA,UACkB;CAClB,MAAM,EAAE,MAAM,UAAU,QAAQ,WAAW,CAAC;CAC5C,MAAM,EAAE,SAAS,aAAa,UAAU,MAAM,MAAM,qBAClD,kCAAkC,OAAO;CAK3C,MAAM,kBAJwB,QAAQ,OAAe,oBAIJ;CAEjD,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,IAAI;EACF,MAAM,aAAa,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE;EACxD,MAAM,cAAc,MAAM,gBAAgB,KAAK,SAAS,UAAU,EAC/D,KAAK,WAAW,EAChB,KAAK,IAAI,EACT,MAAM,QAAQ,EACd,KAAK;EAGR,MAAM,uBAA+C,CAAC;EACtD,IAAI,CAAC,mBAAmB,YAAY,SAAS,GAAG;GAC9C,MAAM,MAAM,YAAY,KAAK,MAAW,EAAE,GAAG;GAC7C,MAAM,SAAS,MAAM,gBAAgB,UAAU,CAC7C,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,EAAE,GAChC,EACE,UAAU,EACR,kBAAkB,EAAE,OAAO,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,EAAE,EAC5D,EACF,CACF,CAAC;GACD,KAAK,MAAM,KAAK,QACd,qBAAqB,OAAO,EAAE,GAAG,KAAK,EAAE,oBAAoB;EAEhE;EAGA,MAAM,iBAAiB,YAAY,OAChC,MAAM,OAAO,EAAE,MAAM,MAAM,OAAO,KAAK,EAAE,CAC5C;EAGA,IAAI,EAFc,OAAO,SAAS,OAAO,KAAK,iBAG5C,OAAO,aAAa,2BAClB,OACA,mBACF;EAGF,MAAM,aAAa,MAAM,gBAAgB,eAAe,OAAO;EAE/D,MAAM,eAAe,wBAAwB;GAC3C,MAAM,YAAY,KAAK,OAAY;IACjC,GAAG;IACH,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE;IACxB,kBAAkB,kBACd,MAAM,QAAQ,EAAE,QAAQ,IACtB,EAAE,SAAS,SACX,IACD,qBAAqB,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM;GACtD,EAAE;GACF;GACA;GACA,YAAY,iBAAiB,UAAU;GACvC;EACF,CAAC;EAED,OAAO,MAAM,KAAK,YAAmB;CACvC,SAAS,OAAO;EACd,OAAO,aAAa,uBAAuB,OAAO,KAAiB;CACrE;AACF"}
@@ -41,7 +41,7 @@ const discoverUrlsFromSitemap = async (targetUrl) => {
41
41
  };
42
42
  const startRecursiveAuditJob = async (targetUrl, userId, urls) => {
43
43
  const existingJob = await AuditJobModel.findOne({
44
- targetUrl,
44
+ targetUrl: String(targetUrl),
45
45
  status: { $in: ["pending", "running"] }
46
46
  });
47
47
  if (existingJob) return existingJob._id.toString();
@@ -1 +1 @@
1
- {"version":3,"file":"recursiveAudit.service.mjs","names":[],"sources":["../../../../src/services/audit/recursiveAudit.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { AuditJobModel, AuditJobStatus } from '@models/auditJob.model';\nimport { AuditPageModel, AuditPageStatus } from '@models/auditPage.model';\nimport { load } from 'cheerio';\nimport { mutateScore, type Score } from './analysis/calculateScore';\nimport { runSingleAudit } from './seoAudit.service';\n\nconst SLEEP_TIME = 30000;\nconst MAX_PAGES = 10;\n\nlet isProcessing = false;\n\n/**\n * Fetches sitemap.xml for the given URL and extracts all <loc> entries.\n * Falls back to [targetUrl] if no sitemap is found.\n */\nexport const discoverUrlsFromSitemap = async (\n targetUrl: string\n): Promise<string[]> => {\n try {\n const { origin } = new URL(targetUrl);\n const sitemapUrl = `${origin}/sitemap.xml`;\n\n const response = await fetch(sitemapUrl, {\n method: 'GET',\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO-Audit-Bot/1.0)' },\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) return [targetUrl];\n\n const sitemapContent = await response.text();\n const $ = load(sitemapContent, { xmlMode: true });\n\n const urls: string[] = [];\n\n // Primary <loc> entries\n $('loc').each((_, el) => {\n const url = $(el).text().trim();\n if (url) urls.push(url);\n });\n\n // Alternate hreflang URLs from <xhtml:link rel=\"alternate\" href=\"...\">\n // Cheerio in xmlMode parses these as \"xhtml:link\" elements\n $('xhtml\\\\:link[rel=\"alternate\"], link[rel=\"alternate\"]').each((_, el) => {\n const href = $(el).attr('href')?.trim();\n if (href && href !== 'x-default') urls.push(href);\n });\n\n const uniqueUrls = [...new Set(urls)];\n return uniqueUrls.length > 0 ? uniqueUrls : [targetUrl];\n } catch {\n return [targetUrl];\n }\n};\n\nexport const startRecursiveAuditJob = async (\n targetUrl: string,\n userId?: string,\n urls?: string[]\n): Promise<string> => {\n const existingJob = await AuditJobModel.findOne({\n targetUrl,\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n });\n\n if (existingJob) {\n return (existingJob._id as any).toString();\n }\n\n const pageUrls =\n urls && urls.length > 0\n ? [...new Set(urls)].slice(0, MAX_PAGES)\n : [targetUrl];\n\n const job = await AuditJobModel.create({\n targetUrl,\n userId,\n status: AuditJobStatus.PENDING,\n totalPageCount: pageUrls.length,\n });\n\n for (const url of pageUrls) {\n await AuditPageModel.create({\n jobId: job._id,\n url,\n status: AuditPageStatus.PENDING,\n }).catch(() => {\n /* ignore duplicate key errors */\n });\n }\n\n processAuditJobs().catch((err) => logger.error(err));\n\n return (job._id as any).toString();\n};\n\nexport const cancelAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.CANCELLED,\n });\n return !!result;\n};\n\nexport const pauseAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.PAUSED,\n });\n return !!result;\n};\n\nexport const resumeAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.RUNNING,\n });\n if (!result) return false;\n processAuditJobs().catch((err) => logger.error(err));\n return true;\n};\n\nexport const processAuditJobs = async (): Promise<void> => {\n if (isProcessing) return;\n isProcessing = true;\n\n try {\n while (true) {\n const job = await AuditJobModel.findOne({\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n }).sort({ createdAt: 1 });\n\n if (!job) break;\n\n if (job.status === AuditJobStatus.PENDING) {\n job.status = AuditJobStatus.RUNNING;\n await job.save();\n }\n\n // Re-fetch to detect external cancellation / pause between pages\n const freshJob = await AuditJobModel.findById(job._id);\n if (\n !freshJob ||\n freshJob.status === AuditJobStatus.CANCELLED ||\n freshJob.status === AuditJobStatus.PAUSED\n ) {\n logger.info(\n `Job ${job._id} is ${freshJob?.status ?? 'missing'} — stopping processor`\n );\n break;\n }\n\n const pendingPage = await AuditPageModel.findOne({\n jobId: job._id,\n status: AuditPageStatus.PENDING,\n });\n\n if (!pendingPage) {\n const hasMorePages = await AuditPageModel.exists({\n jobId: job._id,\n status: { $in: [AuditPageStatus.PENDING, AuditPageStatus.RUNNING] },\n });\n\n if (!hasMorePages) {\n job.status = AuditJobStatus.COMPLETED;\n job.progress = 100;\n await job.save();\n }\n break;\n }\n\n pendingPage.status = AuditPageStatus.RUNNING;\n await pendingPage.save();\n\n try {\n const { events } = await runSingleAudit(pendingPage.url, () => {});\n\n // Compute score the same way the single-page SSE controller does\n let score: Score = { score: 0, totalScore: 0 };\n for (const event of events) {\n score = mutateScore(score, event);\n }\n\n pendingPage.status = AuditPageStatus.COMPLETED;\n pendingPage.results = events;\n pendingPage.score = Math.round(\n score.totalScore > 0 ? (score.score / score.totalScore) * 100 : 0\n );\n await pendingPage.save();\n\n const totalPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n });\n const completedPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n status: AuditPageStatus.COMPLETED,\n });\n\n job.totalPageCount = totalPages;\n job.completedPageCount = completedPages;\n job.progress = Math.round((completedPages / totalPages) * 100);\n await job.save();\n } catch (err) {\n logger.error(`Failed to audit page ${pendingPage.url}:`, err);\n pendingPage.status = AuditPageStatus.FAILED;\n pendingPage.error = String(err);\n await pendingPage.save();\n }\n\n await new Promise((resolve) => setTimeout(resolve, SLEEP_TIME));\n }\n } finally {\n isProcessing = false;\n }\n};\n\nexport const getAuditJobStatus = async (jobId: string) => {\n const job = await AuditJobModel.findById(jobId);\n if (!job) return null;\n\n const pages = await AuditPageModel.find({ jobId }).select(\n 'url status score error results'\n );\n\n return { job, pages };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"recursiveAudit.service.mjs","names":[],"sources":["../../../../src/services/audit/recursiveAudit.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { AuditJobModel, AuditJobStatus } from '@models/auditJob.model';\nimport { AuditPageModel, AuditPageStatus } from '@models/auditPage.model';\nimport { load } from 'cheerio';\nimport { mutateScore, type Score } from './analysis/calculateScore';\nimport { runSingleAudit } from './seoAudit.service';\n\nconst SLEEP_TIME = 30000;\nconst MAX_PAGES = 10;\n\nlet isProcessing = false;\n\n/**\n * Fetches sitemap.xml for the given URL and extracts all <loc> entries.\n * Falls back to [targetUrl] if no sitemap is found.\n */\nexport const discoverUrlsFromSitemap = async (\n targetUrl: string\n): Promise<string[]> => {\n try {\n const { origin } = new URL(targetUrl);\n const sitemapUrl = `${origin}/sitemap.xml`;\n\n const response = await fetch(sitemapUrl, {\n method: 'GET',\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO-Audit-Bot/1.0)' },\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) return [targetUrl];\n\n const sitemapContent = await response.text();\n const $ = load(sitemapContent, { xmlMode: true });\n\n const urls: string[] = [];\n\n // Primary <loc> entries\n $('loc').each((_, el) => {\n const url = $(el).text().trim();\n if (url) urls.push(url);\n });\n\n // Alternate hreflang URLs from <xhtml:link rel=\"alternate\" href=\"...\">\n // Cheerio in xmlMode parses these as \"xhtml:link\" elements\n $('xhtml\\\\:link[rel=\"alternate\"], link[rel=\"alternate\"]').each((_, el) => {\n const href = $(el).attr('href')?.trim();\n if (href && href !== 'x-default') urls.push(href);\n });\n\n const uniqueUrls = [...new Set(urls)];\n return uniqueUrls.length > 0 ? uniqueUrls : [targetUrl];\n } catch {\n return [targetUrl];\n }\n};\n\nexport const startRecursiveAuditJob = async (\n targetUrl: string,\n userId?: string,\n urls?: string[]\n): Promise<string> => {\n const existingJob = await AuditJobModel.findOne({\n targetUrl: String(targetUrl),\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n });\n\n if (existingJob) {\n return (existingJob._id as any).toString();\n }\n\n const pageUrls =\n urls && urls.length > 0\n ? [...new Set(urls)].slice(0, MAX_PAGES)\n : [targetUrl];\n\n const job = await AuditJobModel.create({\n targetUrl,\n userId,\n status: AuditJobStatus.PENDING,\n totalPageCount: pageUrls.length,\n });\n\n for (const url of pageUrls) {\n await AuditPageModel.create({\n jobId: job._id,\n url,\n status: AuditPageStatus.PENDING,\n }).catch(() => {\n /* ignore duplicate key errors */\n });\n }\n\n processAuditJobs().catch((err) => logger.error(err));\n\n return (job._id as any).toString();\n};\n\nexport const cancelAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.CANCELLED,\n });\n return !!result;\n};\n\nexport const pauseAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.PAUSED,\n });\n return !!result;\n};\n\nexport const resumeAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.RUNNING,\n });\n if (!result) return false;\n processAuditJobs().catch((err) => logger.error(err));\n return true;\n};\n\nexport const processAuditJobs = async (): Promise<void> => {\n if (isProcessing) return;\n isProcessing = true;\n\n try {\n while (true) {\n const job = await AuditJobModel.findOne({\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n }).sort({ createdAt: 1 });\n\n if (!job) break;\n\n if (job.status === AuditJobStatus.PENDING) {\n job.status = AuditJobStatus.RUNNING;\n await job.save();\n }\n\n // Re-fetch to detect external cancellation / pause between pages\n const freshJob = await AuditJobModel.findById(job._id);\n if (\n !freshJob ||\n freshJob.status === AuditJobStatus.CANCELLED ||\n freshJob.status === AuditJobStatus.PAUSED\n ) {\n logger.info(\n `Job ${job._id} is ${freshJob?.status ?? 'missing'} — stopping processor`\n );\n break;\n }\n\n const pendingPage = await AuditPageModel.findOne({\n jobId: job._id,\n status: AuditPageStatus.PENDING,\n });\n\n if (!pendingPage) {\n const hasMorePages = await AuditPageModel.exists({\n jobId: job._id,\n status: { $in: [AuditPageStatus.PENDING, AuditPageStatus.RUNNING] },\n });\n\n if (!hasMorePages) {\n job.status = AuditJobStatus.COMPLETED;\n job.progress = 100;\n await job.save();\n }\n break;\n }\n\n pendingPage.status = AuditPageStatus.RUNNING;\n await pendingPage.save();\n\n try {\n const { events } = await runSingleAudit(pendingPage.url, () => {});\n\n // Compute score the same way the single-page SSE controller does\n let score: Score = { score: 0, totalScore: 0 };\n for (const event of events) {\n score = mutateScore(score, event);\n }\n\n pendingPage.status = AuditPageStatus.COMPLETED;\n pendingPage.results = events;\n pendingPage.score = Math.round(\n score.totalScore > 0 ? (score.score / score.totalScore) * 100 : 0\n );\n await pendingPage.save();\n\n const totalPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n });\n const completedPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n status: AuditPageStatus.COMPLETED,\n });\n\n job.totalPageCount = totalPages;\n job.completedPageCount = completedPages;\n job.progress = Math.round((completedPages / totalPages) * 100);\n await job.save();\n } catch (err) {\n logger.error(`Failed to audit page ${pendingPage.url}:`, err);\n pendingPage.status = AuditPageStatus.FAILED;\n pendingPage.error = String(err);\n await pendingPage.save();\n }\n\n await new Promise((resolve) => setTimeout(resolve, SLEEP_TIME));\n }\n } finally {\n isProcessing = false;\n }\n};\n\nexport const getAuditJobStatus = async (jobId: string) => {\n const job = await AuditJobModel.findById(jobId);\n if (!job) return null;\n\n const pages = await AuditPageModel.find({ jobId }).select(\n 'url status score error results'\n );\n\n return { job, pages };\n};\n"],"mappings":"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"}
@@ -29,10 +29,10 @@ const createCliSessionToken = async (userId, organizationId, projectId) => {
29
29
  };
30
30
  const getCliSessionTokenContext = async (token) => {
31
31
  if (!isCliSessionToken(token)) throw new GenericError("INVALID_ACCESS_TOKEN");
32
- const stored = await CliSessionTokenModel.findOne({ token });
32
+ const stored = await CliSessionTokenModel.findOne({ token: String(token) });
33
33
  if (!stored) throw new GenericError("INVALID_ACCESS_TOKEN");
34
34
  if (/* @__PURE__ */ new Date() > stored.expiresAt) {
35
- await CliSessionTokenModel.deleteOne({ token });
35
+ await CliSessionTokenModel.deleteOne({ token: String(token) });
36
36
  throw new GenericError("EXPIRED_ACCESS_TOKEN");
37
37
  }
38
38
  const [user, project, organization] = await Promise.all([
@@ -1 +1 @@
1
- {"version":3,"file":"cliSessionToken.service.mjs","names":[],"sources":["../../../src/services/cliSessionToken.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { CliSessionTokenModel } from '@models/cliSessionToken.model';\nimport { getOrganizationById } from '@services/organization.service';\nimport { getProjectById } from '@services/project.service';\nimport { getUserById } from '@services/user.service';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport type { Types } from 'mongoose';\nimport type { SessionContext } from '@/types/session.types';\n\nexport const CLI_SESSION_TOKEN_PREFIX = 'clisession_';\nconst CLI_SESSION_EXPIRES_MS = 2 * 60 * 60 * 1000; // 2 hours\n\nexport const isCliSessionToken = (token: string): boolean =>\n token.startsWith(CLI_SESSION_TOKEN_PREFIX);\n\nexport const createCliSessionToken = async (\n userId: string | Types.ObjectId,\n organizationId: string,\n projectId: string\n): Promise<{ token: string; expiresAt: Date }> => {\n const token = CLI_SESSION_TOKEN_PREFIX + randomBytes(32).toString('hex');\n const expiresAt = new Date(Date.now() + CLI_SESSION_EXPIRES_MS);\n\n await CliSessionTokenModel.create({\n token,\n userId,\n organizationId,\n projectId,\n expiresAt,\n });\n\n return { token, expiresAt };\n};\n\nexport const getCliSessionTokenContext = async (\n token: string\n): Promise<SessionContext> => {\n if (!isCliSessionToken(token)) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const stored = await CliSessionTokenModel.findOne({ token });\n\n if (!stored) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n if (new Date() > stored.expiresAt) {\n await CliSessionTokenModel.deleteOne({ token });\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n const [user, project, organization] = await Promise.all([\n getUserById(String(stored.userId)),\n getProjectById(stored.projectId),\n getOrganizationById(stored.organizationId),\n ]);\n\n if (!user || !project || !organization) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n return {\n user: mapUserToAPI(user),\n project: mapProjectToAPI(project),\n organization: mapOrganizationToAPI(organization),\n authType: 'session',\n };\n};\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,2BAA2B;AACxC,MAAM,yBAAyB,OAAc;AAE7C,MAAa,qBAAqB,UAChC,MAAM,WAAW,wBAAwB;AAE3C,MAAa,wBAAwB,OACnC,QACA,gBACA,cACgD;CAChD,MAAM,QAAQ,2BAA2B,YAAY,EAAE,EAAE,SAAS,KAAK;CACvE,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,sBAAsB;CAE9D,MAAM,qBAAqB,OAAO;EAChC;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,OAAO;EAAE;EAAO;CAAU;AAC5B;AAEA,MAAa,4BAA4B,OACvC,UAC4B;CAC5B,IAAI,CAAC,kBAAkB,KAAK,GAC1B,MAAM,IAAI,aAAa,sBAAsB;CAG/C,MAAM,SAAS,MAAM,qBAAqB,QAAQ,EAAE,MAAM,CAAC;CAE3D,IAAI,CAAC,QACH,MAAM,IAAI,aAAa,sBAAsB;CAG/C,oBAAI,IAAI,KAAK,IAAI,OAAO,WAAW;EACjC,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAC9C,MAAM,IAAI,aAAa,sBAAsB;CAC/C;CAEA,MAAM,CAAC,MAAM,SAAS,gBAAgB,MAAM,QAAQ,IAAI;EACtD,YAAY,OAAO,OAAO,MAAM,CAAC;EACjC,eAAe,OAAO,SAAS;EAC/B,oBAAoB,OAAO,cAAc;CAC3C,CAAC;CAED,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,cACxB,MAAM,IAAI,aAAa,sBAAsB;CAG/C,OAAO;EACL,MAAM,aAAa,IAAI;EACvB,SAAS,gBAAgB,OAAO;EAChC,cAAc,qBAAqB,YAAY;EAC/C,UAAU;CACZ;AACF"}
1
+ {"version":3,"file":"cliSessionToken.service.mjs","names":[],"sources":["../../../src/services/cliSessionToken.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { CliSessionTokenModel } from '@models/cliSessionToken.model';\nimport { getOrganizationById } from '@services/organization.service';\nimport { getProjectById } from '@services/project.service';\nimport { getUserById } from '@services/user.service';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport type { Types } from 'mongoose';\nimport type { SessionContext } from '@/types/session.types';\n\nexport const CLI_SESSION_TOKEN_PREFIX = 'clisession_';\nconst CLI_SESSION_EXPIRES_MS = 2 * 60 * 60 * 1000; // 2 hours\n\nexport const isCliSessionToken = (token: string): boolean =>\n token.startsWith(CLI_SESSION_TOKEN_PREFIX);\n\nexport const createCliSessionToken = async (\n userId: string | Types.ObjectId,\n organizationId: string,\n projectId: string\n): Promise<{ token: string; expiresAt: Date }> => {\n const token = CLI_SESSION_TOKEN_PREFIX + randomBytes(32).toString('hex');\n const expiresAt = new Date(Date.now() + CLI_SESSION_EXPIRES_MS);\n\n await CliSessionTokenModel.create({\n token,\n userId,\n organizationId,\n projectId,\n expiresAt,\n });\n\n return { token, expiresAt };\n};\n\nexport const getCliSessionTokenContext = async (\n token: string\n): Promise<SessionContext> => {\n if (!isCliSessionToken(token)) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const stored = await CliSessionTokenModel.findOne({ token: String(token) });\n\n if (!stored) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n if (new Date() > stored.expiresAt) {\n await CliSessionTokenModel.deleteOne({ token: String(token) });\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n const [user, project, organization] = await Promise.all([\n getUserById(String(stored.userId)),\n getProjectById(stored.projectId),\n getOrganizationById(stored.organizationId),\n ]);\n\n if (!user || !project || !organization) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n return {\n user: mapUserToAPI(user),\n project: mapProjectToAPI(project),\n organization: mapOrganizationToAPI(organization),\n authType: 'session',\n };\n};\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,2BAA2B;AACxC,MAAM,yBAAyB,OAAc;AAE7C,MAAa,qBAAqB,UAChC,MAAM,WAAW,wBAAwB;AAE3C,MAAa,wBAAwB,OACnC,QACA,gBACA,cACgD;CAChD,MAAM,QAAQ,2BAA2B,YAAY,EAAE,EAAE,SAAS,KAAK;CACvE,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,sBAAsB;CAE9D,MAAM,qBAAqB,OAAO;EAChC;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,OAAO;EAAE;EAAO;CAAU;AAC5B;AAEA,MAAa,4BAA4B,OACvC,UAC4B;CAC5B,IAAI,CAAC,kBAAkB,KAAK,GAC1B,MAAM,IAAI,aAAa,sBAAsB;CAG/C,MAAM,SAAS,MAAM,qBAAqB,QAAQ,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;CAE1E,IAAI,CAAC,QACH,MAAM,IAAI,aAAa,sBAAsB;CAG/C,oBAAI,IAAI,KAAK,IAAI,OAAO,WAAW;EACjC,MAAM,qBAAqB,UAAU,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;EAC7D,MAAM,IAAI,aAAa,sBAAsB;CAC/C;CAEA,MAAM,CAAC,MAAM,SAAS,gBAAgB,MAAM,QAAQ,IAAI;EACtD,YAAY,OAAO,OAAO,MAAM,CAAC;EACjC,eAAe,OAAO,SAAS;EAC/B,oBAAoB,OAAO,cAAc;CAC3C,CAAC;CAED,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,cACxB,MAAM,IAAI,aAAa,sBAAsB;CAG/C,OAAO;EACL,MAAM,aAAa,IAAI;EACvB,SAAS,gBAAgB,OAAO;EAChC,cAAc,qBAAqB,YAAY;EAC/C,UAAU;CACZ;AACF"}
@@ -128,7 +128,7 @@ const updateDictionaryById = async (dictionaryId, dictionary) => {
128
128
  */
129
129
  const updateDictionaryByKey = async (dictionaryKey, dictionary, projectId) => {
130
130
  const existing = await DictionaryModel.findOne({
131
- key: dictionaryKey,
131
+ key: String(dictionaryKey),
132
132
  projectIds: projectId
133
133
  });
134
134
  if (!existing) throw new GenericError("DICTIONARY_UPDATE_FAILED", { dictionaryKey });
@@ -1 +1 @@
1
- {"version":3,"file":"dictionary.service.mjs","names":[],"sources":["../../../src/services/dictionary.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { DictionaryModel } from '@models/dictionary.model';\nimport { getDemoDictionaries } from '@utils/demoDictionaries';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport type { DictionaryFilters } from '@utils/filtersAndPagination/getDictionaryFiltersAndPagination';\nimport { removeObjectKeys } from '@utils/removeObjectKeys';\nimport {\n type DictionaryFields,\n validateDictionary,\n} from '@utils/validation/validateDictionary';\nimport { Types } from 'mongoose';\nimport type {\n Dictionary,\n DictionaryData,\n DictionaryDocument,\n} from '@/types/dictionary.types';\nimport type { Project } from '@/types/project.types';\n\n/**\n * Finds dictionaries based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @param sortOptions - Sorting options.\n * @param includeContent - Whether to include the dictionary content.\n * @returns List of dictionaries matching the filters.\n */\nexport const findDictionaries = async (\n filters: DictionaryFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>,\n includeContent = true\n): Promise<DictionaryDocument[]> => {\n try {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the filters\n { $match: filters },\n\n // Stage 2: Sort if provided (default handled in filter builder)\n ...(sortOptions && Object.keys(sortOptions).length > 0\n ? [{ $sort: sortOptions }]\n : []),\n\n // Stage 3: Skip for pagination\n { $skip: skip },\n\n // Stage 4: Limit the number of documents\n { $limit: limit },\n\n // Stage 5: Project to include/exclude content\n ...(!includeContent ? [{ $project: { content: 0 } }] : []),\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n } catch (error) {\n logger.error('Error fetching dictionaries:', error);\n throw error;\n }\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\n/**\n * Finds a dictionary by its ID and includes the 'versions' field.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID with available versions.\n */\nexport const getDictionaryById = async (\n dictionaryId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const id = Types.ObjectId.isValid(dictionaryId as string)\n ? new Types.ObjectId(dictionaryId as string)\n : dictionaryId;\n\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by ID\n { $match: { _id: id } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries.length) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return new DictionaryModel(dictionaries[0]);\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryKey - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\nexport const getDictionaryByKey = async (\n dictionaryKey: string,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const dictionaries = await getDictionariesByKeys([dictionaryKey], projectId);\n\n return dictionaries[0];\n};\n\nexport const getDictionariesByKeys = async (\n dictionaryKeys: string[],\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by key\n { $match: { key: { $in: dictionaryKeys }, projectIds: projectId } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries) {\n throw new GenericError('DICTIONARY_NOT_FOUND', {\n dictionaryKeys,\n projectId,\n });\n }\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\nexport const getDictionariesByTags = async (\n tags: string[],\n projectId: string | Project['id']\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by tags\n {\n $match: {\n tags: { $in: tags },\n projectIds: projectId,\n },\n },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\n/**\n * Counts the total number of dictionaries that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of dictionaries.\n */\nexport const countDictionaries = async (\n filters: DictionaryFilters\n): Promise<number> => {\n const result = await DictionaryModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('DICTIONARY_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new dictionary in the database.\n * @param dictionary - The dictionary data to create.\n * @returns The created dictionary.\n */\nexport const createDictionary = async (\n dictionary: DictionaryData\n): Promise<DictionaryDocument> => {\n const errors = await validateDictionary(dictionary);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n errors,\n });\n }\n\n return await DictionaryModel.create(dictionary);\n};\n\n/**\n * Updates an existing dictionary in the database by its ID.\n * @param dictionaryId - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryById = async (\n dictionaryId: string | Types.ObjectId,\n dictionary: Partial<Dictionary>\n): Promise<DictionaryDocument> => {\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as unknown as Partial<Dictionary>;\n\n const updatedKeys = Object.keys(dictionaryToUpdate) as DictionaryFields;\n const errors = await validateDictionary(dictionaryToUpdate, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n dictionaryId,\n errors,\n });\n }\n\n const result = await DictionaryModel.updateOne(\n { _id: dictionaryId },\n dictionaryToUpdate\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryId });\n }\n\n const updatedDictionary = await getDictionaryById(dictionaryId);\n\n return updatedDictionary;\n};\n\n/**\n * Updates an existing dictionary in the database by its key.\n * @param dictionaryKey - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryByKey = async (\n dictionaryKey: string,\n dictionary: Partial<Dictionary>,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const existing = await DictionaryModel.findOne({\n key: dictionaryKey,\n projectIds: projectId,\n });\n\n if (!existing) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryKey });\n }\n\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as Partial<Dictionary>;\n\n // Optional: run your validateDictionary on dictionaryToUpdate here\n\n // Apply updated fields onto the existing doc\n Object.assign(existing, dictionaryToUpdate);\n\n // Mongoose cannot track deep Map mutations done via Object.assign, so we\n // must explicitly mark 'content' as modified, otherwise the new versioned\n // content is silently dropped and the document is saved unchanged.\n existing.markModified('content');\n\n // Save – this will trigger timestamps on parent + subdocs\n await existing.save();\n\n return existing;\n};\n\n/**\n * Deletes a dictionary from the database by its ID.\n * @param dictionaryId - The ID of the dictionary to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteDictionaryById = async (\n dictionaryId: string\n): Promise<DictionaryDocument> => {\n const dictionary = await DictionaryModel.findByIdAndDelete(dictionaryId);\n\n if (!dictionary) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return dictionary;\n};\n\n// Function to extract the numeric part of the version\nconst getVersionNumber = (version: string): number => {\n const match = version.match(/^v(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid version format: ${version}`);\n }\n return parseInt(match[1], 10);\n};\n\nexport const incrementVersion = (dictionary: Dictionary): string => {\n const VERSION_PREFIX = 'v';\n\n const versions = [...(dictionary.content.keys() ?? [])];\n const lastVersion = versions[versions.length - 1];\n\n // Start with the next version number\n let newNumber = getVersionNumber(lastVersion) + 1;\n let newVersion = `${VERSION_PREFIX}${newNumber}`;\n\n // Loop until a unique version is found\n while (versions.includes(newVersion)) {\n newNumber += 1;\n newVersion = `${VERSION_PREFIX}${newNumber}`;\n }\n\n return newVersion;\n};\n\n/**\n * Creates demo dictionaries for a project.\n * @param projectIds - List of project IDs.\n * @param creatorId - The ID of the user creating the demo content.\n */\nexport const createDemoDictionaries = async (\n projectIds: string[],\n creatorId: Types.ObjectId | string\n): Promise<void> => {\n const demoDictionaries = getDemoDictionaries(projectIds, creatorId);\n\n for (const dictionary of demoDictionaries) {\n await createDictionary(dictionary);\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4BA,MAAa,mBAAmB,OAC9B,SACA,OAAO,GACP,QAAQ,KACR,aACA,iBAAiB,SACiB;CAClC,IAAI;EAwBF,QAJyB,MAnBE,gBAAgB,UAA8B;GAEvE,EAAE,QAAQ,QAAQ;GAGlB,GAAI,eAAe,OAAO,KAAK,WAAW,EAAE,SAAS,IACjD,CAAC,EAAE,OAAO,YAAY,CAAC,IACvB,CAAC;GAGL,EAAE,OAAO,KAAK;GAGd,EAAE,QAAQ,MAAM;GAGhB,GAAI,CAAC,iBAAiB,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC;EAC1D,CAAC,GAEqC,KACnC,WAAW,IAAI,gBAAgB,MAAM,CAGlB;CACxB,SAAS,OAAO;EACd,OAAO,MAAM,gCAAgC,KAAK;EAClD,MAAM;CACR;AACF;;;;;;;;;;;AAYA,MAAa,oBAAoB,OAC/B,iBACgC;CAChC,MAAM,KAAK,MAAM,SAAS,QAAQ,YAAsB,IACpD,IAAI,MAAM,SAAS,YAAsB,IACzC;CAEJ,MAAM,eAAe,MAAM,gBAAgB,UAA8B,CAEvE,EAAE,QAAQ,EAAE,KAAK,GAAG,EAAE,GAGtB,EACE,YAAY,EACV,UAAU,EACR,MAAM;EACJ,OAAO,EAAE,gBAAgB,WAAW;EACpC,IAAI;EACJ,IAAI;CACN,EACF,EACF,EACF,CACF,CAAC;CAED,IAAI,CAAC,aAAa,QAChB,MAAM,IAAI,aAAa,wBAAwB,EAAE,aAAa,CAAC;CAGjE,OAAO,IAAI,gBAAgB,aAAa,EAAE;AAC5C;;;;;;AAOA,MAAa,qBAAqB,OAChC,eACA,cACgC;CAGhC,QAAO,MAFoB,sBAAsB,CAAC,aAAa,GAAG,SAAS,GAEvD;AACtB;AAEA,MAAa,wBAAwB,OACnC,gBACA,cACkC;CAClC,MAAM,eAAe,MAAM,gBAAgB,UAA8B,CAEvE,EAAE,QAAQ;EAAE,KAAK,EAAE,KAAK,eAAe;EAAG,YAAY;CAAU,EAAE,GAGlE,EACE,YAAY,EACV,UAAU,EACR,MAAM;EACJ,OAAO,EAAE,gBAAgB,WAAW;EACpC,IAAI;EACJ,IAAI;CACN,EACF,EACF,EACF,CACF,CAAC;CAED,IAAI,CAAC,cACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA;CACF,CAAC;CAOH,OAJyB,aAAa,KACnC,WAAW,IAAI,gBAAgB,MAAM,CAGlB;AACxB;AAEA,MAAa,wBAAwB,OACnC,MACA,cACkC;CA4BlC,QAJyB,MAvBE,gBAAgB,UAA8B,CAEvE,EACE,QAAQ;EACN,MAAM,EAAE,KAAK,KAAK;EAClB,YAAY;CACd,EACF,GAGA,EACE,YAAY,EACV,UAAU,EACR,MAAM;EACJ,OAAO,EAAE,gBAAgB,WAAW;EACpC,IAAI;EACJ,IAAI;CACN,EACF,EACF,EACF,CACF,CAAC,GAEqC,KACnC,WAAW,IAAI,gBAAgB,MAAM,CAGlB;AACxB;;;;;;AAOA,MAAa,oBAAoB,OAC/B,YACoB;CACpB,MAAM,SAAS,MAAM,gBAAgB,eAAe,OAAO;CAE3D,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,aAAa,2BAA2B,EAAE,QAAQ,CAAC;CAG/D,OAAO;AACT;;;;;;AAOA,MAAa,mBAAmB,OAC9B,eACgC;CAChC,MAAM,SAAS,MAAM,mBAAmB,UAAU;CAElD,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,6BAA6B,EAClD,OACF,CAAC;CAGH,OAAO,MAAM,gBAAgB,OAAO,UAAU;AAChD;;;;;;;AAQA,MAAa,uBAAuB,OAClC,cACA,eACgC;CAEhC,MAAM,qBAAqB,iBADF,4BAA4B,UACM,GAAG,CAC5D,IACF,CAAC;CAGD,MAAM,SAAS,MAAM,mBAAmB,oBADpB,OAAO,KAAK,kBACsC,CAAC;CAEvE,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,6BAA6B;EAClD;EACA;CACF,CAAC;CAQH,KAAI,MALiB,gBAAgB,UACnC,EAAE,KAAK,aAAa,GACpB,kBACF,GAEW,iBAAiB,GAC1B,MAAM,IAAI,aAAa,4BAA4B,EAAE,aAAa,CAAC;CAKrE,OAAO,MAFyB,kBAAkB,YAAY;AAGhE;;;;;;;AAQA,MAAa,wBAAwB,OACnC,eACA,YACA,cACgC;CAChC,MAAM,WAAW,MAAM,gBAAgB,QAAQ;EAC7C,KAAK;EACL,YAAY;CACd,CAAC;CAED,IAAI,CAAC,UACH,MAAM,IAAI,aAAa,4BAA4B,EAAE,cAAc,CAAC;CAItE,MAAM,qBAAqB,iBADF,4BAA4B,UACM,GAAG,CAC5D,IACF,CAAC;CAKD,OAAO,OAAO,UAAU,kBAAkB;CAK1C,SAAS,aAAa,SAAS;CAG/B,MAAM,SAAS,KAAK;CAEpB,OAAO;AACT;;;;;;AAOA,MAAa,uBAAuB,OAClC,iBACgC;CAChC,MAAM,aAAa,MAAM,gBAAgB,kBAAkB,YAAY;CAEvE,IAAI,CAAC,YACH,MAAM,IAAI,aAAa,wBAAwB,EAAE,aAAa,CAAC;CAGjE,OAAO;AACT;AAGA,MAAM,oBAAoB,YAA4B;CACpD,MAAM,QAAQ,QAAQ,MAAM,UAAU;CACtC,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,2BAA2B,SAAS;CAEtD,OAAO,SAAS,MAAM,IAAI,EAAE;AAC9B;AAEA,MAAa,oBAAoB,eAAmC;CAClE,MAAM,iBAAiB;CAEvB,MAAM,WAAW,CAAC,GAAI,WAAW,QAAQ,KAAK,KAAK,CAAC,CAAE;CACtD,MAAM,cAAc,SAAS,SAAS,SAAS;CAG/C,IAAI,YAAY,iBAAiB,WAAW,IAAI;CAChD,IAAI,aAAa,GAAG,iBAAiB;CAGrC,OAAO,SAAS,SAAS,UAAU,GAAG;EACpC,aAAa;EACb,aAAa,GAAG,iBAAiB;CACnC;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,yBAAyB,OACpC,YACA,cACkB;CAClB,MAAM,mBAAmB,oBAAoB,YAAY,SAAS;CAElE,KAAK,MAAM,cAAc,kBACvB,MAAM,iBAAiB,UAAU;AAErC"}
1
+ {"version":3,"file":"dictionary.service.mjs","names":[],"sources":["../../../src/services/dictionary.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { DictionaryModel } from '@models/dictionary.model';\nimport { getDemoDictionaries } from '@utils/demoDictionaries';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport type { DictionaryFilters } from '@utils/filtersAndPagination/getDictionaryFiltersAndPagination';\nimport { removeObjectKeys } from '@utils/removeObjectKeys';\nimport {\n type DictionaryFields,\n validateDictionary,\n} from '@utils/validation/validateDictionary';\nimport { Types } from 'mongoose';\nimport type {\n Dictionary,\n DictionaryData,\n DictionaryDocument,\n} from '@/types/dictionary.types';\nimport type { Project } from '@/types/project.types';\n\n/**\n * Finds dictionaries based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @param sortOptions - Sorting options.\n * @param includeContent - Whether to include the dictionary content.\n * @returns List of dictionaries matching the filters.\n */\nexport const findDictionaries = async (\n filters: DictionaryFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>,\n includeContent = true\n): Promise<DictionaryDocument[]> => {\n try {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the filters\n { $match: filters },\n\n // Stage 2: Sort if provided (default handled in filter builder)\n ...(sortOptions && Object.keys(sortOptions).length > 0\n ? [{ $sort: sortOptions }]\n : []),\n\n // Stage 3: Skip for pagination\n { $skip: skip },\n\n // Stage 4: Limit the number of documents\n { $limit: limit },\n\n // Stage 5: Project to include/exclude content\n ...(!includeContent ? [{ $project: { content: 0 } }] : []),\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n } catch (error) {\n logger.error('Error fetching dictionaries:', error);\n throw error;\n }\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\n/**\n * Finds a dictionary by its ID and includes the 'versions' field.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID with available versions.\n */\nexport const getDictionaryById = async (\n dictionaryId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const id = Types.ObjectId.isValid(dictionaryId as string)\n ? new Types.ObjectId(dictionaryId as string)\n : dictionaryId;\n\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by ID\n { $match: { _id: id } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries.length) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return new DictionaryModel(dictionaries[0]);\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryKey - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\nexport const getDictionaryByKey = async (\n dictionaryKey: string,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const dictionaries = await getDictionariesByKeys([dictionaryKey], projectId);\n\n return dictionaries[0];\n};\n\nexport const getDictionariesByKeys = async (\n dictionaryKeys: string[],\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by key\n { $match: { key: { $in: dictionaryKeys }, projectIds: projectId } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries) {\n throw new GenericError('DICTIONARY_NOT_FOUND', {\n dictionaryKeys,\n projectId,\n });\n }\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\nexport const getDictionariesByTags = async (\n tags: string[],\n projectId: string | Project['id']\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by tags\n {\n $match: {\n tags: { $in: tags },\n projectIds: projectId,\n },\n },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\n/**\n * Counts the total number of dictionaries that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of dictionaries.\n */\nexport const countDictionaries = async (\n filters: DictionaryFilters\n): Promise<number> => {\n const result = await DictionaryModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('DICTIONARY_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new dictionary in the database.\n * @param dictionary - The dictionary data to create.\n * @returns The created dictionary.\n */\nexport const createDictionary = async (\n dictionary: DictionaryData\n): Promise<DictionaryDocument> => {\n const errors = await validateDictionary(dictionary);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n errors,\n });\n }\n\n return await DictionaryModel.create(dictionary);\n};\n\n/**\n * Updates an existing dictionary in the database by its ID.\n * @param dictionaryId - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryById = async (\n dictionaryId: string | Types.ObjectId,\n dictionary: Partial<Dictionary>\n): Promise<DictionaryDocument> => {\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as unknown as Partial<Dictionary>;\n\n const updatedKeys = Object.keys(dictionaryToUpdate) as DictionaryFields;\n const errors = await validateDictionary(dictionaryToUpdate, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n dictionaryId,\n errors,\n });\n }\n\n const result = await DictionaryModel.updateOne(\n { _id: dictionaryId },\n dictionaryToUpdate\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryId });\n }\n\n const updatedDictionary = await getDictionaryById(dictionaryId);\n\n return updatedDictionary;\n};\n\n/**\n * Updates an existing dictionary in the database by its key.\n * @param dictionaryKey - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryByKey = async (\n dictionaryKey: string,\n dictionary: Partial<Dictionary>,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const existing = await DictionaryModel.findOne({\n key: String(dictionaryKey),\n projectIds: projectId,\n });\n\n if (!existing) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryKey });\n }\n\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as Partial<Dictionary>;\n\n // Optional: run your validateDictionary on dictionaryToUpdate here\n\n // Apply updated fields onto the existing doc\n Object.assign(existing, dictionaryToUpdate);\n\n // Mongoose cannot track deep Map mutations done via Object.assign, so we\n // must explicitly mark 'content' as modified, otherwise the new versioned\n // content is silently dropped and the document is saved unchanged.\n existing.markModified('content');\n\n // Save – this will trigger timestamps on parent + subdocs\n await existing.save();\n\n return existing;\n};\n\n/**\n * Deletes a dictionary from the database by its ID.\n * @param dictionaryId - The ID of the dictionary to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteDictionaryById = async (\n dictionaryId: string\n): Promise<DictionaryDocument> => {\n const dictionary = await DictionaryModel.findByIdAndDelete(dictionaryId);\n\n if (!dictionary) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return dictionary;\n};\n\n// Function to extract the numeric part of the version\nconst getVersionNumber = (version: string): number => {\n const match = version.match(/^v(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid version format: ${version}`);\n }\n return parseInt(match[1], 10);\n};\n\nexport const incrementVersion = (dictionary: Dictionary): string => {\n const VERSION_PREFIX = 'v';\n\n const versions = [...(dictionary.content.keys() ?? [])];\n const lastVersion = versions[versions.length - 1];\n\n // Start with the next version number\n let newNumber = getVersionNumber(lastVersion) + 1;\n let newVersion = `${VERSION_PREFIX}${newNumber}`;\n\n // Loop until a unique version is found\n while (versions.includes(newVersion)) {\n newNumber += 1;\n newVersion = `${VERSION_PREFIX}${newNumber}`;\n }\n\n return newVersion;\n};\n\n/**\n * Creates demo dictionaries for a project.\n * @param projectIds - List of project IDs.\n * @param creatorId - The ID of the user creating the demo content.\n */\nexport const createDemoDictionaries = async (\n projectIds: string[],\n creatorId: Types.ObjectId | string\n): Promise<void> => {\n const demoDictionaries = getDemoDictionaries(projectIds, creatorId);\n\n for (const dictionary of demoDictionaries) {\n await createDictionary(dictionary);\n }\n};\n"],"mappings":"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"}
@@ -30,7 +30,7 @@ const generateClientCredentials = () => {
30
30
  * @returns The an object containing the client, the rights and the project or false if not found
31
31
  */
32
32
  const getClientAndProjectByClientId = async (clientId) => {
33
- const project = await ProjectModel.findOne({ "oAuth2Access.clientId": clientId });
33
+ const project = await ProjectModel.findOne({ "oAuth2Access.clientId": String(clientId) });
34
34
  if (!project) return false;
35
35
  const oAuth2Access = project.oAuth2Access.find((access) => access.clientId === clientId);
36
36
  if (!oAuth2Access) return false;
@@ -127,7 +127,7 @@ const saveToken = async (token, client, user) => {
127
127
  */
128
128
  const extendOAuth2AccessToken = async (accessToken) => {
129
129
  const nextExpiresAt = getTokenExpireAt();
130
- await OAuth2AccessTokenModel.updateOne({ accessToken }, { $set: {
130
+ await OAuth2AccessTokenModel.updateOne({ accessToken: String(accessToken) }, { $set: {
131
131
  accessTokenExpiresAt: nextExpiresAt,
132
132
  expiresIn: nextExpiresAt
133
133
  } });
@@ -140,7 +140,7 @@ const extendOAuth2AccessToken = async (accessToken) => {
140
140
  * @returns The access token or false if not found
141
141
  */
142
142
  const getAccessToken = async (accessToken) => {
143
- const token = await OAuth2AccessTokenModel.findOne({ accessToken });
143
+ const token = await OAuth2AccessTokenModel.findOne({ accessToken: String(accessToken) });
144
144
  if (!token) return false;
145
145
  const currentExpiresAt = token.accessTokenExpiresAt ?? token.expiresIn;
146
146
  if (currentExpiresAt && shouldExtendOAuth2Token(currentExpiresAt)) {
@@ -1 +1 @@
1
- {"version":3,"file":"oAuth2.service.mjs","names":[],"sources":["../../../src/services/oAuth2.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { OAuth2AccessTokenModel } from '@models/oAuth2.model';\nimport { ProjectModel } from '@models/project.model';\nimport type { Callback, Client } from '@node-oauth/oauth2-server';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport { getTokenExpireAt, shouldExtendOAuth2Token } from '@utils/oAuth2';\nimport type { Types } from 'mongoose';\nimport type { OAuth2Token } from '@/types/oAuth2.types';\nimport type { Organization } from '@/types/organization.types';\nimport type {\n OAuth2Access,\n OAuth2AccessContext,\n Project,\n ProjectDocument,\n} from '@/types/project.types';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\nimport type { Token } from '../schemas/oAuth2.schema';\nimport { getOrganizationById } from './organization.service';\nimport { getUserById } from './user.service';\n\n/**\n * Function to generate client credentials\n *\n * @returns The client id and client secret\n */\nexport const generateClientCredentials = (): {\n clientId: string;\n clientSecret: string;\n} => {\n const clientId = randomBytes(16).toString('hex'); // Generate a 16 character hexadecimal string\n const clientSecret = randomBytes(32).toString('hex'); // Generate a 32 character hexadecimal string\n\n return { clientId, clientSecret };\n};\n\n/**\n * Method to get the client and the project\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The an object containing the client, the rights and the project or false if not found\n */\nexport const getClientAndProjectByClientId = async (\n clientId: string\n): Promise<\n | {\n client: Client;\n oAuth2Access: OAuth2Access;\n project: ProjectDocument;\n grants: Token['grants'];\n }\n | false\n> => {\n const project = await ProjectModel.findOne({\n 'oAuth2Access.clientId': clientId,\n });\n\n if (!project) {\n return false;\n }\n\n const oAuth2Access = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!oAuth2Access) {\n return false;\n }\n\n const formattedClient: Client = {\n id: oAuth2Access.clientId,\n clientId,\n clientSecret: oAuth2Access.clientSecret,\n grants: ['client_credentials'],\n };\n\n return {\n client: formattedClient,\n oAuth2Access,\n grants: oAuth2Access.grants,\n project,\n };\n};\n\n/**\n * Get the client and verify that the client secret is correct\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The client or false if not found\n */\nexport const getClient = async (\n clientId: string,\n clientSecret: string\n): Promise<Client | false> => {\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client } = result;\n\n if (!client || client.clientSecret !== clientSecret) {\n return false;\n }\n\n return client;\n};\n\n/**\n * Format an OAuth2Token\n *\n * @param token - The token to format\n * @param client - The client\n * @param user - The user\n * @param project - The project\n * @param organization - The organization\n * @param grants - The grants\n * @returns The formatted token\n */\nexport const formatOAuth2Token = (\n token: Token,\n client: Client,\n user: UserAPI,\n project: Project,\n organization: Organization,\n grants: Token['grants']\n): OAuth2Token => {\n const { clientId, userId, ...restToken } = token;\n\n if (String(userId) !== String(user.id)) {\n throw new GenericError('USER_ID_MISMATCH');\n }\n\n const formattedToken: OAuth2Token = {\n ...restToken,\n client,\n user: mapUserToAPI(user),\n organization: mapOrganizationToAPI(organization),\n project: mapProjectToAPI(project),\n accessToken: token.accessToken,\n accessTokenExpiresAt: token.accessTokenExpiresAt ?? new Date('999-99-99'),\n grants,\n };\n\n return formattedToken;\n};\n\n/**\n * Format a auth token for the database\n *\n * @param token - The oAuth2 token to format\n * @param clientId - The client ID\n * @param userId - The user ID\n * @returns\n */\nexport const formatDBToken = (\n token: OAuth2Token,\n clientId: Client['id'],\n userId: User['id'] | string\n): Token => {\n const formattedToken: Token = {\n id: token.id,\n clientId: clientId,\n userId: userId as Types.ObjectId,\n accessToken: token.accessToken,\n expiresIn: token.accessTokenExpiresAt ?? getTokenExpireAt(),\n };\n\n return formattedToken;\n};\n\n/**\n * Method to save the token\n *\n * @param token - The token\n * @param client - The client\n * @param user - The user\n * @returns The saved token or false if not saved\n */\nexport const saveToken = async (\n token: OAuth2Token,\n client: Client,\n user: UserAPI\n): Promise<OAuth2Token | false> => {\n const formattedAccessToken: Token = formatDBToken(token, client.id, user.id);\n\n const result = await OAuth2AccessTokenModel.create(formattedAccessToken);\n\n if (!result) {\n return false;\n }\n\n const result2 = await getClientAndProjectByClientId(result.clientId);\n\n if (!result2) {\n return false;\n }\n\n const { project } = result2;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedResult = formatOAuth2Token(\n formattedAccessToken,\n client,\n user,\n project,\n organization,\n token.rights\n );\n return formattedResult;\n};\n\n/**\n * Sliding-refresh: push the token's expiry forward when it has been used\n * within the refresh threshold. Idempotent and cheap when no extension is due.\n */\nexport const extendOAuth2AccessToken = async (\n accessToken: string\n): Promise<Date> => {\n const nextExpiresAt = getTokenExpireAt();\n await OAuth2AccessTokenModel.updateOne(\n { accessToken },\n { $set: { accessTokenExpiresAt: nextExpiresAt, expiresIn: nextExpiresAt } }\n );\n return nextExpiresAt;\n};\n\n/**\n * Method to get the access token\n *\n * @param accessToken - The access token\n * @returns The access token or false if not found\n */\nexport const getAccessToken = async (\n accessToken: string\n): Promise<OAuth2Token | false> => {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken,\n });\n\n if (!token) {\n return false;\n }\n\n // Slide the expiry forward when this active token is approaching its\n // deadline so a long-lived integration doesn't have to re-authenticate.\n const currentExpiresAt = token.accessTokenExpiresAt ?? token.expiresIn;\n if (currentExpiresAt && shouldExtendOAuth2Token(currentExpiresAt)) {\n const nextExpiresAt = await extendOAuth2AccessToken(accessToken);\n token.accessTokenExpiresAt = nextExpiresAt;\n token.expiresIn = nextExpiresAt;\n }\n\n const { userId, clientId } = token;\n\n const user = await getUserById(userId);\n\n if (!user) {\n return false;\n }\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client, project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedAccessToken = formatOAuth2Token(\n token,\n client,\n user,\n project,\n organization,\n grants\n );\n\n return formattedAccessToken;\n};\n\n/**\n * Method to get the user from the client\n *\n * @param client - The client\n * @returns The user or false if not found\n */\nexport const getUserFromClient = async (\n client: Client\n): Promise<UserDocument | false> => {\n const response = await getClientAndProjectByClientId(client.id);\n\n if (!response) {\n return false;\n }\n\n const { userId } = response.oAuth2Access;\n\n if (!userId) {\n return false;\n }\n\n const user = await getUserById(userId);\n\n return user ?? false;\n};\n\n/**\n * Method to verify the permissions (grants)\n *\n * @param token - The token\n * @param scope - The scope\n * @returns True if the token has the required scope, false otherwise\n */\nexport const verifyScope = async (\n _token: OAuth2Token,\n _scope: string,\n _callback?: Callback<boolean> | undefined\n): Promise<boolean> => {\n // Implement the verification of scopes if necessary\n return true;\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const validateOAuth2AccessToken = async (\n accessToken: string\n): Promise<Token> => {\n try {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken,\n });\n\n if (!token) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n // Check if token is expired\n if (new Date() > new Date(token.expiresIn)) {\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n return ensureMongoDocumentToObject(token);\n } catch (_error) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const getOAuth2AccessTokenContext = async (\n token: Token\n): Promise<OAuth2AccessContext> => {\n const { userId, clientId } = token;\n\n const user = await getUserById(String(userId));\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const { project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n return {\n accessToken: token.accessToken,\n user: user ? mapUserToAPI(user) : undefined,\n project: project ? mapProjectToAPI(project) : undefined,\n organization: organization ? mapOrganizationToAPI(organization) : undefined,\n grants,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"oAuth2.service.mjs","names":[],"sources":["../../../src/services/oAuth2.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { OAuth2AccessTokenModel } from '@models/oAuth2.model';\nimport { ProjectModel } from '@models/project.model';\nimport type { Callback, Client } from '@node-oauth/oauth2-server';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport { getTokenExpireAt, shouldExtendOAuth2Token } from '@utils/oAuth2';\nimport type { Types } from 'mongoose';\nimport type { OAuth2Token } from '@/types/oAuth2.types';\nimport type { Organization } from '@/types/organization.types';\nimport type {\n OAuth2Access,\n OAuth2AccessContext,\n Project,\n ProjectDocument,\n} from '@/types/project.types';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\nimport type { Token } from '../schemas/oAuth2.schema';\nimport { getOrganizationById } from './organization.service';\nimport { getUserById } from './user.service';\n\n/**\n * Function to generate client credentials\n *\n * @returns The client id and client secret\n */\nexport const generateClientCredentials = (): {\n clientId: string;\n clientSecret: string;\n} => {\n const clientId = randomBytes(16).toString('hex'); // Generate a 16 character hexadecimal string\n const clientSecret = randomBytes(32).toString('hex'); // Generate a 32 character hexadecimal string\n\n return { clientId, clientSecret };\n};\n\n/**\n * Method to get the client and the project\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The an object containing the client, the rights and the project or false if not found\n */\nexport const getClientAndProjectByClientId = async (\n clientId: string\n): Promise<\n | {\n client: Client;\n oAuth2Access: OAuth2Access;\n project: ProjectDocument;\n grants: Token['grants'];\n }\n | false\n> => {\n const project = await ProjectModel.findOne({\n 'oAuth2Access.clientId': String(clientId),\n });\n\n if (!project) {\n return false;\n }\n\n const oAuth2Access = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!oAuth2Access) {\n return false;\n }\n\n const formattedClient: Client = {\n id: oAuth2Access.clientId,\n clientId,\n clientSecret: oAuth2Access.clientSecret,\n grants: ['client_credentials'],\n };\n\n return {\n client: formattedClient,\n oAuth2Access,\n grants: oAuth2Access.grants,\n project,\n };\n};\n\n/**\n * Get the client and verify that the client secret is correct\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The client or false if not found\n */\nexport const getClient = async (\n clientId: string,\n clientSecret: string\n): Promise<Client | false> => {\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client } = result;\n\n if (!client || client.clientSecret !== clientSecret) {\n return false;\n }\n\n return client;\n};\n\n/**\n * Format an OAuth2Token\n *\n * @param token - The token to format\n * @param client - The client\n * @param user - The user\n * @param project - The project\n * @param organization - The organization\n * @param grants - The grants\n * @returns The formatted token\n */\nexport const formatOAuth2Token = (\n token: Token,\n client: Client,\n user: UserAPI,\n project: Project,\n organization: Organization,\n grants: Token['grants']\n): OAuth2Token => {\n const { clientId, userId, ...restToken } = token;\n\n if (String(userId) !== String(user.id)) {\n throw new GenericError('USER_ID_MISMATCH');\n }\n\n const formattedToken: OAuth2Token = {\n ...restToken,\n client,\n user: mapUserToAPI(user),\n organization: mapOrganizationToAPI(organization),\n project: mapProjectToAPI(project),\n accessToken: token.accessToken,\n accessTokenExpiresAt: token.accessTokenExpiresAt ?? new Date('999-99-99'),\n grants,\n };\n\n return formattedToken;\n};\n\n/**\n * Format a auth token for the database\n *\n * @param token - The oAuth2 token to format\n * @param clientId - The client ID\n * @param userId - The user ID\n * @returns\n */\nexport const formatDBToken = (\n token: OAuth2Token,\n clientId: Client['id'],\n userId: User['id'] | string\n): Token => {\n const formattedToken: Token = {\n id: token.id,\n clientId: clientId,\n userId: userId as Types.ObjectId,\n accessToken: token.accessToken,\n expiresIn: token.accessTokenExpiresAt ?? getTokenExpireAt(),\n };\n\n return formattedToken;\n};\n\n/**\n * Method to save the token\n *\n * @param token - The token\n * @param client - The client\n * @param user - The user\n * @returns The saved token or false if not saved\n */\nexport const saveToken = async (\n token: OAuth2Token,\n client: Client,\n user: UserAPI\n): Promise<OAuth2Token | false> => {\n const formattedAccessToken: Token = formatDBToken(token, client.id, user.id);\n\n const result = await OAuth2AccessTokenModel.create(formattedAccessToken);\n\n if (!result) {\n return false;\n }\n\n const result2 = await getClientAndProjectByClientId(result.clientId);\n\n if (!result2) {\n return false;\n }\n\n const { project } = result2;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedResult = formatOAuth2Token(\n formattedAccessToken,\n client,\n user,\n project,\n organization,\n token.rights\n );\n return formattedResult;\n};\n\n/**\n * Sliding-refresh: push the token's expiry forward when it has been used\n * within the refresh threshold. Idempotent and cheap when no extension is due.\n */\nexport const extendOAuth2AccessToken = async (\n accessToken: string\n): Promise<Date> => {\n const nextExpiresAt = getTokenExpireAt();\n await OAuth2AccessTokenModel.updateOne(\n { accessToken: String(accessToken) },\n { $set: { accessTokenExpiresAt: nextExpiresAt, expiresIn: nextExpiresAt } }\n );\n return nextExpiresAt;\n};\n\n/**\n * Method to get the access token\n *\n * @param accessToken - The access token\n * @returns The access token or false if not found\n */\nexport const getAccessToken = async (\n accessToken: string\n): Promise<OAuth2Token | false> => {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken: String(accessToken),\n });\n\n if (!token) {\n return false;\n }\n\n // Slide the expiry forward when this active token is approaching its\n // deadline so a long-lived integration doesn't have to re-authenticate.\n const currentExpiresAt = token.accessTokenExpiresAt ?? token.expiresIn;\n if (currentExpiresAt && shouldExtendOAuth2Token(currentExpiresAt)) {\n const nextExpiresAt = await extendOAuth2AccessToken(accessToken);\n token.accessTokenExpiresAt = nextExpiresAt;\n token.expiresIn = nextExpiresAt;\n }\n\n const { userId, clientId } = token;\n\n const user = await getUserById(userId);\n\n if (!user) {\n return false;\n }\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client, project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedAccessToken = formatOAuth2Token(\n token,\n client,\n user,\n project,\n organization,\n grants\n );\n\n return formattedAccessToken;\n};\n\n/**\n * Method to get the user from the client\n *\n * @param client - The client\n * @returns The user or false if not found\n */\nexport const getUserFromClient = async (\n client: Client\n): Promise<UserDocument | false> => {\n const response = await getClientAndProjectByClientId(client.id);\n\n if (!response) {\n return false;\n }\n\n const { userId } = response.oAuth2Access;\n\n if (!userId) {\n return false;\n }\n\n const user = await getUserById(userId);\n\n return user ?? false;\n};\n\n/**\n * Method to verify the permissions (grants)\n *\n * @param token - The token\n * @param scope - The scope\n * @returns True if the token has the required scope, false otherwise\n */\nexport const verifyScope = async (\n _token: OAuth2Token,\n _scope: string,\n _callback?: Callback<boolean> | undefined\n): Promise<boolean> => {\n // Implement the verification of scopes if necessary\n return true;\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const validateOAuth2AccessToken = async (\n accessToken: string\n): Promise<Token> => {\n try {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken,\n });\n\n if (!token) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n // Check if token is expired\n if (new Date() > new Date(token.expiresIn)) {\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n return ensureMongoDocumentToObject(token);\n } catch (_error) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const getOAuth2AccessTokenContext = async (\n token: Token\n): Promise<OAuth2AccessContext> => {\n const { userId, clientId } = token;\n\n const user = await getUserById(String(userId));\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const { project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n return {\n accessToken: token.accessToken,\n user: user ? mapUserToAPI(user) : undefined,\n project: project ? mapProjectToAPI(project) : undefined,\n organization: organization ? mapOrganizationToAPI(organization) : undefined,\n grants,\n };\n};\n"],"mappings":"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"}
@@ -69,7 +69,7 @@ const deleteAccessKey = async (clientId, project, userId) => {
69
69
  const refreshAccessKey = async (clientId, projectId, userId) => {
70
70
  const project = await ProjectModel.findOne({
71
71
  _id: projectId,
72
- "oAuth2Access.clientId": clientId,
72
+ "oAuth2Access.clientId": String(clientId),
73
73
  "oAuth2Access.userId": String(userId)
74
74
  });
75
75
  if (!project) throw new GenericError("PROJECT_NOT_FOUND", {
@@ -84,7 +84,7 @@ const refreshAccessKey = async (clientId, projectId, userId) => {
84
84
  });
85
85
  const { clientSecret } = generateClientCredentials();
86
86
  if ((await ProjectModel.updateOne({
87
- "oAuth2Access.clientId": clientId,
87
+ "oAuth2Access.clientId": String(clientId),
88
88
  "oAuth2Access.userId": String(userId)
89
89
  }, { $set: {
90
90
  "oAuth2Access.$.clientId": projectAccess.clientId,
@@ -1 +1 @@
1
- {"version":3,"file":"projectAccessKey.service.mjs","names":[],"sources":["../../../src/services/projectAccessKey.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { ProjectModel } from '@models/project.model';\nimport { GenericError } from '@utils/errors';\nimport type { Types } from 'mongoose';\nimport type {\n AccessKeyData,\n OAuth2Access,\n OAuth2AccessData,\n Project,\n} from '@/types/project.types';\nimport type { User } from '@/types/user.types';\nimport { getProjectById } from './project.service';\n\n/**\n * Generates cryptographically secure OAuth2 client credentials\n *\n * @returns Object containing clientId and clientSecret\n *\n * Security improvements:\n * - clientId: 32 characters (128 bits of entropy)\n * - clientSecret: 64 characters (256 bits of entropy)\n * - Uses crypto.randomBytes for cryptographically secure random generation\n * - Follows OAuth2 best practices for credential strength\n */\nconst generateClientCredentials = () => ({\n clientId: randomBytes(16).toString('hex'), // 32 character hexadecimal string\n clientSecret: randomBytes(32).toString('hex'), // 64 character hexadecimal string\n});\n\n/**\n * Adds a new access key to a project.\n *\n * @param accessKeyData - The access key data.\n * @param projectId - The ID of the project to add the access key to.\n * @param user - The user adding the access key.\n * @returns The new access key.\n *\n */\nexport const addNewAccessKey = async (\n accessKeyData: AccessKeyData,\n projectId: string | Types.ObjectId,\n user: User\n): Promise<OAuth2Access> => {\n const { clientId, clientSecret } = generateClientCredentials();\n\n const newAccessKey: OAuth2AccessData = {\n ...accessKeyData,\n clientId,\n clientSecret,\n userId: user.id,\n accessToken: [],\n grants: accessKeyData.grants,\n };\n\n const result = await ProjectModel.updateOne(\n { _id: projectId },\n { $push: { oAuth2Access: newAccessKey } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n return newAccessKeyId;\n};\n\nexport const deleteAccessKey = async (\n clientId: string | Types.ObjectId,\n project: Project,\n userId: string | Types.ObjectId\n) => {\n const projectAccess = project.oAuth2Access.find(\n (access) =>\n access.clientId === clientId && String(access.userId) === String(userId)\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': clientId,\n 'oAuth2Access.userId': String(userId),\n },\n { $pull: { oAuth2Access: { clientId } } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_DELETION_FAILED', {\n clientId,\n projectId: project.id,\n });\n }\n\n return projectAccess;\n};\n\nexport const refreshAccessKey = async (\n clientId: string | Types.ObjectId,\n projectId: string | Types.ObjectId,\n userId: string | Types.ObjectId\n): Promise<OAuth2Access> => {\n const project = await ProjectModel.findOne({\n _id: projectId,\n 'oAuth2Access.clientId': clientId,\n 'oAuth2Access.userId': String(userId),\n });\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_FOUND', {\n clientId,\n projectId,\n userId,\n });\n }\n\n const projectAccess = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const { clientSecret } = generateClientCredentials();\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': clientId,\n 'oAuth2Access.userId': String(userId),\n },\n {\n $set: {\n 'oAuth2Access.$.clientId': projectAccess.clientId,\n 'oAuth2Access.$.clientSecret': clientSecret,\n },\n }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_UPDATE_FAILED', {\n clientId,\n projectId,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === projectAccess.clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData: updatedProject.oAuth2Access,\n projectId,\n userId,\n });\n }\n\n return newAccessKeyId;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,MAAM,mCAAmC;CACvC,UAAU,YAAY,EAAE,EAAE,SAAS,KAAK;CACxC,cAAc,YAAY,EAAE,EAAE,SAAS,KAAK;AAC9C;;;;;;;;;;AAWA,MAAa,kBAAkB,OAC7B,eACA,WACA,SAC0B;CAC1B,MAAM,EAAE,UAAU,iBAAiB,0BAA0B;CAE7D,MAAM,eAAiC;EACrC,GAAG;EACH;EACA;EACA,QAAQ,KAAK;EACb,aAAa,CAAC;EACd,QAAQ,cAAc;CACxB;CAOA,KAAI,MALiB,aAAa,UAChC,EAAE,KAAK,UAAU,GACjB,EAAE,OAAO,EAAE,cAAc,aAAa,EAAE,CAC1C,GAEW,kBAAkB,GAC3B,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;CACf,CAAC;CAKH,MAAM,kBAAiB,MAFM,eAAe,SAAS,GAEf,aAAa,MAChD,WAAW,OAAO,aAAa,QAClC;CAEA,IAAI,CAAC,gBACH,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;CACf,CAAC;CAGH,OAAO;AACT;AAEA,MAAa,kBAAkB,OAC7B,UACA,SACA,WACG;CACH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WACC,OAAO,aAAa,YAAY,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM,CAC3E;CAEA,IAAI,CAAC,eACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;CACrB,CAAC;CAWH,KAAI,MARiB,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,MAAM;CACtC,GACA,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,CAC1C,GAEW,kBAAkB,GAC3B,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA,WAAW,QAAQ;CACrB,CAAC;CAGH,OAAO;AACT;AAEA,MAAa,mBAAmB,OAC9B,UACA,WACA,WAC0B;CAC1B,MAAM,UAAU,MAAM,aAAa,QAAQ;EACzC,KAAK;EACL,yBAAyB;EACzB,uBAAuB,OAAO,MAAM;CACtC,CAAC;CAED,IAAI,CAAC,SACH,MAAM,IAAI,aAAa,qBAAqB;EAC1C;EACA;EACA;CACF,CAAC;CAGH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WAAW,OAAO,aAAa,QAClC;CAEA,IAAI,CAAC,eACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;CACrB,CAAC;CAGH,MAAM,EAAE,iBAAiB,0BAA0B;CAenD,KAAI,MAbiB,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,MAAM;CACtC,GACA,EACE,MAAM;EACJ,2BAA2B,cAAc;EACzC,+BAA+B;CACjC,EACF,CACF,GAEW,kBAAkB,GAC3B,MAAM,IAAI,aAAa,4BAA4B;EACjD;EACA;CACF,CAAC;CAGH,MAAM,iBAAiB,MAAM,eAAe,SAAS;CAErD,MAAM,iBAAiB,eAAe,aAAa,MAChD,WAAW,OAAO,aAAa,cAAc,QAChD;CAEA,IAAI,CAAC,gBACH,MAAM,IAAI,aAAa,8BAA8B;EACnD,eAAe,eAAe;EAC9B;EACA;CACF,CAAC;CAGH,OAAO;AACT"}
1
+ {"version":3,"file":"projectAccessKey.service.mjs","names":[],"sources":["../../../src/services/projectAccessKey.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { ProjectModel } from '@models/project.model';\nimport { GenericError } from '@utils/errors';\nimport type { Types } from 'mongoose';\nimport type {\n AccessKeyData,\n OAuth2Access,\n OAuth2AccessData,\n Project,\n} from '@/types/project.types';\nimport type { User } from '@/types/user.types';\nimport { getProjectById } from './project.service';\n\n/**\n * Generates cryptographically secure OAuth2 client credentials\n *\n * @returns Object containing clientId and clientSecret\n *\n * Security improvements:\n * - clientId: 32 characters (128 bits of entropy)\n * - clientSecret: 64 characters (256 bits of entropy)\n * - Uses crypto.randomBytes for cryptographically secure random generation\n * - Follows OAuth2 best practices for credential strength\n */\nconst generateClientCredentials = () => ({\n clientId: randomBytes(16).toString('hex'), // 32 character hexadecimal string\n clientSecret: randomBytes(32).toString('hex'), // 64 character hexadecimal string\n});\n\n/**\n * Adds a new access key to a project.\n *\n * @param accessKeyData - The access key data.\n * @param projectId - The ID of the project to add the access key to.\n * @param user - The user adding the access key.\n * @returns The new access key.\n *\n */\nexport const addNewAccessKey = async (\n accessKeyData: AccessKeyData,\n projectId: string | Types.ObjectId,\n user: User\n): Promise<OAuth2Access> => {\n const { clientId, clientSecret } = generateClientCredentials();\n\n const newAccessKey: OAuth2AccessData = {\n ...accessKeyData,\n clientId,\n clientSecret,\n userId: user.id,\n accessToken: [],\n grants: accessKeyData.grants,\n };\n\n const result = await ProjectModel.updateOne(\n { _id: projectId },\n { $push: { oAuth2Access: newAccessKey } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n return newAccessKeyId;\n};\n\nexport const deleteAccessKey = async (\n clientId: string | Types.ObjectId,\n project: Project,\n userId: string | Types.ObjectId\n) => {\n const projectAccess = project.oAuth2Access.find(\n (access) =>\n access.clientId === clientId && String(access.userId) === String(userId)\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': clientId,\n 'oAuth2Access.userId': String(userId),\n },\n { $pull: { oAuth2Access: { clientId } } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_DELETION_FAILED', {\n clientId,\n projectId: project.id,\n });\n }\n\n return projectAccess;\n};\n\nexport const refreshAccessKey = async (\n clientId: string | Types.ObjectId,\n projectId: string | Types.ObjectId,\n userId: string | Types.ObjectId\n): Promise<OAuth2Access> => {\n const project = await ProjectModel.findOne({\n _id: projectId,\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n });\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_FOUND', {\n clientId,\n projectId,\n userId,\n });\n }\n\n const projectAccess = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const { clientSecret } = generateClientCredentials();\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n },\n {\n $set: {\n 'oAuth2Access.$.clientId': projectAccess.clientId,\n 'oAuth2Access.$.clientSecret': clientSecret,\n },\n }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_UPDATE_FAILED', {\n clientId,\n projectId,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === projectAccess.clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData: updatedProject.oAuth2Access,\n projectId,\n userId,\n });\n }\n\n return newAccessKeyId;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,MAAM,mCAAmC;CACvC,UAAU,YAAY,EAAE,EAAE,SAAS,KAAK;CACxC,cAAc,YAAY,EAAE,EAAE,SAAS,KAAK;AAC9C;;;;;;;;;;AAWA,MAAa,kBAAkB,OAC7B,eACA,WACA,SAC0B;CAC1B,MAAM,EAAE,UAAU,iBAAiB,0BAA0B;CAE7D,MAAM,eAAiC;EACrC,GAAG;EACH;EACA;EACA,QAAQ,KAAK;EACb,aAAa,CAAC;EACd,QAAQ,cAAc;CACxB;CAOA,KAAI,MALiB,aAAa,UAChC,EAAE,KAAK,UAAU,GACjB,EAAE,OAAO,EAAE,cAAc,aAAa,EAAE,CAC1C,GAEW,kBAAkB,GAC3B,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;CACf,CAAC;CAKH,MAAM,kBAAiB,MAFM,eAAe,SAAS,GAEf,aAAa,MAChD,WAAW,OAAO,aAAa,QAClC;CAEA,IAAI,CAAC,gBACH,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;CACf,CAAC;CAGH,OAAO;AACT;AAEA,MAAa,kBAAkB,OAC7B,UACA,SACA,WACG;CACH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WACC,OAAO,aAAa,YAAY,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM,CAC3E;CAEA,IAAI,CAAC,eACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;CACrB,CAAC;CAWH,KAAI,MARiB,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,MAAM;CACtC,GACA,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,CAC1C,GAEW,kBAAkB,GAC3B,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA,WAAW,QAAQ;CACrB,CAAC;CAGH,OAAO;AACT;AAEA,MAAa,mBAAmB,OAC9B,UACA,WACA,WAC0B;CAC1B,MAAM,UAAU,MAAM,aAAa,QAAQ;EACzC,KAAK;EACL,yBAAyB,OAAO,QAAQ;EACxC,uBAAuB,OAAO,MAAM;CACtC,CAAC;CAED,IAAI,CAAC,SACH,MAAM,IAAI,aAAa,qBAAqB;EAC1C;EACA;EACA;CACF,CAAC;CAGH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WAAW,OAAO,aAAa,QAClC;CAEA,IAAI,CAAC,eACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;CACrB,CAAC;CAGH,MAAM,EAAE,iBAAiB,0BAA0B;CAenD,KAAI,MAbiB,aAAa,UAChC;EACE,yBAAyB,OAAO,QAAQ;EACxC,uBAAuB,OAAO,MAAM;CACtC,GACA,EACE,MAAM;EACJ,2BAA2B,cAAc;EACzC,+BAA+B;CACjC,EACF,CACF,GAEW,kBAAkB,GAC3B,MAAM,IAAI,aAAa,4BAA4B;EACjD;EACA;CACF,CAAC;CAGH,MAAM,iBAAiB,MAAM,eAAe,SAAS;CAErD,MAAM,iBAAiB,eAAe,aAAa,MAChD,WAAW,OAAO,aAAa,cAAc,QAChD;CAEA,IAAI,CAAC,gBACH,MAAM,IAAI,aAAa,8BAA8B;EACnD,eAAe,eAAe;EAC9B;EACA;CACF,CAAC;CAGH,OAAO;AACT"}
@@ -56,7 +56,7 @@ const findShowcaseProjectByUrl = async (websiteUrl) => {
56
56
  try {
57
57
  hostname = new URL(websiteUrl).hostname;
58
58
  } catch {
59
- return await ShowcaseProjectModel.findOne({ websiteUrl }).lean();
59
+ return await ShowcaseProjectModel.findOne({ websiteUrl: String(websiteUrl) }).lean();
60
60
  }
61
61
  const hostnameRegex = new RegExp(`^https?://${hostname.replace(/\./g, "\\.")}(/|$)`, "i");
62
62
  return await ShowcaseProjectModel.findOne({ websiteUrl: { $regex: hostnameRegex } }).lean();