@intlayer/backend 7.5.12 → 7.5.14

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 (100) hide show
  1. package/dist/assets/utils/AI/askDocQuestion/embeddings/blog/en/per-component_vs_centralized_i18n.json +6158 -0
  2. package/dist/assets/utils/AI/askDocQuestion/embeddings/frequent_questions/en/error-vite-env-only.json +2054 -0
  3. package/dist/esm/controllers/ai.controller.mjs.map +1 -1
  4. package/dist/esm/controllers/bitbucket.controller.mjs.map +1 -1
  5. package/dist/esm/controllers/dictionary.controller.mjs.map +1 -1
  6. package/dist/esm/controllers/eventListener.controller.mjs.map +1 -1
  7. package/dist/esm/controllers/github.controller.mjs.map +1 -1
  8. package/dist/esm/controllers/gitlab.controller.mjs.map +1 -1
  9. package/dist/esm/controllers/project.controller.mjs.map +1 -1
  10. package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
  11. package/dist/esm/controllers/tag.controller.mjs.map +1 -1
  12. package/dist/esm/controllers/user.controller.mjs.map +1 -1
  13. package/dist/esm/emails/InviteUserEmail.mjs.map +1 -1
  14. package/dist/esm/emails/MagicLinkEmail.mjs.map +1 -1
  15. package/dist/esm/emails/OAuthTokenCreatedEmail.mjs.map +1 -1
  16. package/dist/esm/emails/PasswordChangeConfirmation.mjs.map +1 -1
  17. package/dist/esm/emails/ResetUserPassword.mjs.map +1 -1
  18. package/dist/esm/emails/SubscriptionPaymentCancellation.mjs.map +1 -1
  19. package/dist/esm/emails/SubscriptionPaymentError.mjs.map +1 -1
  20. package/dist/esm/emails/SubscriptionPaymentSuccess.mjs.map +1 -1
  21. package/dist/esm/emails/ValidateUserEmail.mjs.map +1 -1
  22. package/dist/esm/emails/Welcome.mjs.map +1 -1
  23. package/dist/esm/index.mjs.map +1 -1
  24. package/dist/esm/services/bitbucket.service.mjs.map +1 -1
  25. package/dist/esm/services/github.service.mjs.map +1 -1
  26. package/dist/esm/services/gitlab.service.mjs.map +1 -1
  27. package/dist/esm/services/oAuth2.service.mjs.map +1 -1
  28. package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
  29. package/dist/esm/services/subscription.service.mjs.map +1 -1
  30. package/dist/esm/services/user.service.mjs.map +1 -1
  31. package/dist/esm/services/webhook.service.mjs.map +1 -1
  32. package/dist/esm/utils/AI/askDocQuestion/askDocQuestion.mjs.map +1 -1
  33. package/dist/esm/utils/AI/askDocQuestion/embeddings/blog/en/per-component_vs_centralized_i18n.json +6158 -0
  34. package/dist/esm/utils/AI/askDocQuestion/embeddings/frequent_questions/en/error-vite-env-only.json +2054 -0
  35. package/dist/esm/utils/AI/askDocQuestion/indexMarkdownFiles.mjs.map +1 -1
  36. package/dist/esm/utils/AI/auditDictionary/index.mjs.map +1 -1
  37. package/dist/esm/utils/AI/auditDictionaryField/index.mjs.map +1 -1
  38. package/dist/esm/utils/AI/auditDictionaryMetadata/index.mjs.map +1 -1
  39. package/dist/esm/utils/AI/auditTag/index.mjs.map +1 -1
  40. package/dist/esm/utils/AI/autocomplete/index.mjs.map +1 -1
  41. package/dist/esm/utils/AI/customQuery/index.mjs.map +1 -1
  42. package/dist/esm/utils/AI/translateJSON/index.mjs.map +1 -1
  43. package/dist/esm/utils/accessControl.mjs.map +1 -1
  44. package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
  45. package/dist/esm/utils/cors.mjs.map +1 -1
  46. package/dist/esm/utils/ensureArrayQueryFilter.mjs.map +1 -1
  47. package/dist/esm/utils/ensureMongoDocumentToObject.mjs.map +1 -1
  48. package/dist/esm/utils/filtersAndPagination/getDictionaryFiltersAndPagination.mjs.map +1 -1
  49. package/dist/esm/utils/filtersAndPagination/getDiscussionFiltersAndPagination.mjs.map +1 -1
  50. package/dist/esm/utils/filtersAndPagination/getOrganizationFiltersAndPagination.mjs.map +1 -1
  51. package/dist/esm/utils/filtersAndPagination/getProjectFiltersAndPagination.mjs.map +1 -1
  52. package/dist/esm/utils/filtersAndPagination/getTagFiltersAndPagination.mjs.map +1 -1
  53. package/dist/esm/utils/filtersAndPagination/getUserFiltersAndPagination.mjs.map +1 -1
  54. package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
  55. package/dist/esm/utils/oAuth2.mjs.map +1 -1
  56. package/dist/esm/utils/permissions.mjs.map +1 -1
  57. package/dist/esm/utils/plan.mjs.map +1 -1
  58. package/dist/esm/utils/rateLimiter.mjs.map +1 -1
  59. package/dist/esm/utils/validation/validateArray.mjs.map +1 -1
  60. package/dist/esm/utils/validation/validateDictionary.mjs.map +1 -1
  61. package/dist/esm/utils/validation/validateOrganization.mjs.map +1 -1
  62. package/dist/esm/utils/validation/validateProject.mjs.map +1 -1
  63. package/dist/esm/utils/validation/validateString.mjs.map +1 -1
  64. package/dist/esm/utils/validation/validateTag.mjs.map +1 -1
  65. package/dist/esm/utils/validation/validateUser.mjs.map +1 -1
  66. package/dist/esm/webhooks/stripe.webhook.mjs.map +1 -1
  67. package/dist/types/controllers/ai.controller.d.ts.map +1 -1
  68. package/dist/types/controllers/gitlab.controller.d.ts.map +1 -1
  69. package/dist/types/emails/InviteUserEmail.d.ts +4 -4
  70. package/dist/types/emails/InviteUserEmail.d.ts.map +1 -1
  71. package/dist/types/emails/MagicLinkEmail.d.ts +4 -4
  72. package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +4 -4
  73. package/dist/types/emails/PasswordChangeConfirmation.d.ts +4 -4
  74. package/dist/types/emails/ResetUserPassword.d.ts +4 -4
  75. package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +4 -4
  76. package/dist/types/emails/SubscriptionPaymentError.d.ts +4 -4
  77. package/dist/types/emails/SubscriptionPaymentError.d.ts.map +1 -1
  78. package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +4 -4
  79. package/dist/types/emails/ValidateUserEmail.d.ts +4 -4
  80. package/dist/types/emails/Welcome.d.ts +4 -4
  81. package/dist/types/models/dictionary.model.d.ts +4 -4
  82. package/dist/types/models/discussion.model.d.ts +3 -3
  83. package/dist/types/models/oAuth2.model.d.ts +3 -3
  84. package/dist/types/schemas/dictionary.schema.d.ts +6 -6
  85. package/dist/types/schemas/discussion.schema.d.ts +6 -6
  86. package/dist/types/schemas/oAuth2.schema.d.ts +5 -5
  87. package/dist/types/schemas/organization.schema.d.ts +6 -6
  88. package/dist/types/schemas/plans.schema.d.ts +6 -6
  89. package/dist/types/schemas/plans.schema.d.ts.map +1 -1
  90. package/dist/types/schemas/project.schema.d.ts +6 -6
  91. package/dist/types/schemas/project.schema.d.ts.map +1 -1
  92. package/dist/types/schemas/session.schema.d.ts +6 -6
  93. package/dist/types/schemas/tag.schema.d.ts +6 -6
  94. package/dist/types/schemas/user.schema.d.ts +6 -6
  95. package/dist/types/schemas/user.schema.d.ts.map +1 -1
  96. package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts +2 -2
  97. package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts +2 -2
  98. package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts +2 -2
  99. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +2 -2
  100. package/package.json +20 -20
@@ -1 +1 @@
1
- {"version":3,"file":"github.service.mjs","names":["error: any","sha: string | undefined"],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\nimport type { Project } from '@/types/project.types';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'github',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'github',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n\ntype DispatchEventOptions = {\n project: Project;\n eventType?: string;\n payload?: Record<string, any>;\n};\n\nexport const triggerGithubDispatch = async ({\n project,\n eventType = 'intlayer_cms_update',\n payload = {},\n}: DispatchEventOptions) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository || repository.provider !== 'github') {\n throw new Error('Project is not connected to a GitHub repository.');\n }\n\n // Get the valid Access Token\n // Assuming the first token is the active one, or implement logic to find the specific user's token\n const tokenData = oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0]; // Assuming array of tokens\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found for GitHub.');\n }\n\n const { owner, repository: repoName } = repository;\n const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;\n\n try {\n // 2. Send the Dispatch Event\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/vnd.github+json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_type: eventType,\n client_payload: {\n ...payload,\n projectId: project.id,\n timestamp: Date.now(),\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`\n );\n return true;\n } catch (error) {\n logger.error(error);\n throw error;\n }\n};\n\n/**\n * Check if a GitHub workflow file exists\n */\nexport const checkWorkflowFileExists = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking workflow file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitHub workflow file\n */\nexport const createWorkflowFile = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI workflow'\n): Promise<void> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Check if file exists to get SHA for update\n let sha: string | undefined;\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n\n if (Array.isArray(data) || !('sha' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n sha = data.sha;\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n // Encode content to base64\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filename,\n message,\n content: encodedContent,\n branch,\n ...(sha && { sha }), // Include SHA if updating existing file\n });\n\n logger.info(\n `Successfully ${sha ? 'updated' : 'created'} workflow file ${filename} for ${owner}/${repo}`\n );\n } catch (error) {\n logger.error('Error creating/updating workflow file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"github.service.mjs","names":[],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\nimport type { Project } from '@/types/project.types';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'github',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'github',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n\ntype DispatchEventOptions = {\n project: Project;\n eventType?: string;\n payload?: Record<string, any>;\n};\n\nexport const triggerGithubDispatch = async ({\n project,\n eventType = 'intlayer_cms_update',\n payload = {},\n}: DispatchEventOptions) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository || repository.provider !== 'github') {\n throw new Error('Project is not connected to a GitHub repository.');\n }\n\n // Get the valid Access Token\n // Assuming the first token is the active one, or implement logic to find the specific user's token\n const tokenData = oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0]; // Assuming array of tokens\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found for GitHub.');\n }\n\n const { owner, repository: repoName } = repository;\n const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;\n\n try {\n // 2. Send the Dispatch Event\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/vnd.github+json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_type: eventType,\n client_payload: {\n ...payload,\n projectId: project.id,\n timestamp: Date.now(),\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`\n );\n return true;\n } catch (error) {\n logger.error(error);\n throw error;\n }\n};\n\n/**\n * Check if a GitHub workflow file exists\n */\nexport const checkWorkflowFileExists = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking workflow file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitHub workflow file\n */\nexport const createWorkflowFile = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI workflow'\n): Promise<void> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Check if file exists to get SHA for update\n let sha: string | undefined;\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n\n if (Array.isArray(data) || !('sha' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n sha = data.sha;\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n // Encode content to base64\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filename,\n message,\n content: encodedContent,\n branch,\n ...(sha && { sha }), // Include SHA if updating existing file\n });\n\n logger.info(\n `Successfully ${sha ? 'updated' : 'created'} workflow file ${filename} for ${owner}/${repo}`\n );\n } catch (error) {\n logger.error('Error creating/updating workflow file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"gitlab.service.mjs","names":["error: any","existingContentSha: string | undefined","body: any"],"sources":["../../../src/services/gitlab.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\n\nconst GITLAB_DEFAULT_URL = 'https://gitlab.com';\n\nexport type GitLabProject = {\n id: number;\n name: string;\n path_with_namespace: string;\n web_url: string;\n default_branch: string;\n visibility: string;\n last_activity_at: string;\n namespace: {\n id: number;\n name: string;\n path: string;\n };\n};\n\nexport type GitLabTreeItem = {\n id: string;\n name: string;\n type: 'tree' | 'blob';\n path: string;\n mode: string;\n};\n\n/**\n * Get GitLab authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (\n redirectUri: string,\n instanceUrl?: string,\n login?: string\n): string => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId) {\n throw new Error('GitLab Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: 'api read_repository',\n state: 'gitlab_oauth',\n });\n\n if (login) {\n params.append('login_hint', login);\n }\n\n return `${baseUrl}/oauth/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange GitLab authorization code for access token\n */\nexport const exchangeCodeForToken = async (\n code: string,\n redirectUri: string,\n instanceUrl?: string\n): Promise<string> => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const clientSecret = process.env.GITLAB_CLIENT_SECRET;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitLab OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(`${baseUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`GitLab token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitLab token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitLab code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's GitLab projects/repositories\n */\nexport const getUserProjects = async (\n accessToken: string,\n instanceUrl?: string\n): Promise<GitLabProject[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const response = await fetch(\n `${baseUrl}/api/v4/projects?membership=true&order_by=last_activity_at&per_page=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch GitLab projects: ${response.statusText}`\n );\n }\n\n const projects: GitLabProject[] = await response.json();\n return projects;\n } catch (error) {\n logger.error('Error fetching GitLab projects:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a GitLab repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n projectId: number,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n // Use GitLab's repository tree API with recursive option\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const tree: GitLabTreeItem[] = await response.json();\n\n // Filter files that match the configuration candidates\n const foundFiles = tree\n .filter((item) => {\n if (item.type !== 'blob') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on GitLab:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from GitLab and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n projectId: number,\n path: string,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string | null> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(path);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching GitLab file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get GitLab access token from user's linked account\n */\nexport const getGitLabTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'gitlab',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'gitlab',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'gitlab',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'gitlab',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitLab token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a GitLab CI pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n branch: string = 'main',\n instanceUrl?: string\n): Promise<boolean> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitLab CI pipeline file\n */\nexport const createPipelineFile = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n content: string,\n branch: string = 'main',\n instanceUrl?: string,\n message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n // Check if file exists to get content_sha256 for update\n let existingContentSha: string | undefined;\n try {\n const checkResponse = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (checkResponse.ok) {\n const fileData = await checkResponse.json();\n existingContentSha = fileData.content_sha256;\n }\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n const body: any = {\n branch,\n content: encodedContent,\n commit_message: message,\n encoding: 'base64',\n };\n\n if (existingContentSha) {\n body.last_commit_id = existingContentSha;\n }\n\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}`,\n {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to create/update pipeline file: ${errorText}`);\n }\n\n logger.info(\n `Successfully ${existingContentSha ? 'updated' : 'created'} pipeline file ${filename} for project ${projectId}`\n );\n } catch (error) {\n logger.error('Error creating/updating pipeline file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"gitlab.service.mjs","names":[],"sources":["../../../src/services/gitlab.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\n\nconst GITLAB_DEFAULT_URL = 'https://gitlab.com';\n\nexport type GitLabProject = {\n id: number;\n name: string;\n path_with_namespace: string;\n web_url: string;\n default_branch: string;\n visibility: string;\n last_activity_at: string;\n namespace: {\n id: number;\n name: string;\n path: string;\n };\n};\n\nexport type GitLabTreeItem = {\n id: string;\n name: string;\n type: 'tree' | 'blob';\n path: string;\n mode: string;\n};\n\n/**\n * Get GitLab authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (\n redirectUri: string,\n instanceUrl?: string,\n login?: string\n): string => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId) {\n throw new Error('GitLab Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: 'api read_repository',\n state: 'gitlab_oauth',\n });\n\n if (login) {\n params.append('login_hint', login);\n }\n\n return `${baseUrl}/oauth/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange GitLab authorization code for access token\n */\nexport const exchangeCodeForToken = async (\n code: string,\n redirectUri: string,\n instanceUrl?: string\n): Promise<string> => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const clientSecret = process.env.GITLAB_CLIENT_SECRET;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitLab OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(`${baseUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`GitLab token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitLab token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitLab code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's GitLab projects/repositories\n */\nexport const getUserProjects = async (\n accessToken: string,\n instanceUrl?: string\n): Promise<GitLabProject[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const response = await fetch(\n `${baseUrl}/api/v4/projects?membership=true&order_by=last_activity_at&per_page=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch GitLab projects: ${response.statusText}`\n );\n }\n\n const projects: GitLabProject[] = await response.json();\n return projects;\n } catch (error) {\n logger.error('Error fetching GitLab projects:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a GitLab repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n projectId: number,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n // Use GitLab's repository tree API with recursive option\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const tree: GitLabTreeItem[] = await response.json();\n\n // Filter files that match the configuration candidates\n const foundFiles = tree\n .filter((item) => {\n if (item.type !== 'blob') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on GitLab:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from GitLab and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n projectId: number,\n path: string,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string | null> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(path);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching GitLab file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get GitLab access token from user's linked account\n */\nexport const getGitLabTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'gitlab',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'gitlab',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'gitlab',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'gitlab',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitLab token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a GitLab CI pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n branch: string = 'main',\n instanceUrl?: string\n): Promise<boolean> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitLab CI pipeline file\n */\nexport const createPipelineFile = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n content: string,\n branch: string = 'main',\n instanceUrl?: string,\n message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n // Check if file exists to get content_sha256 for update\n let existingContentSha: string | undefined;\n try {\n const checkResponse = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (checkResponse.ok) {\n const fileData = await checkResponse.json();\n existingContentSha = fileData.content_sha256;\n }\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n const body: any = {\n branch,\n content: encodedContent,\n commit_message: message,\n encoding: 'base64',\n };\n\n if (existingContentSha) {\n body.last_commit_id = existingContentSha;\n }\n\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}`,\n {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to create/update pipeline file: ${errorText}`);\n }\n\n logger.info(\n `Successfully ${existingContentSha ? 'updated' : 'created'} pipeline file ${filename} for project ${projectId}`\n );\n } catch (error) {\n logger.error('Error creating/updating pipeline file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"oAuth2.service.mjs","names":["formattedAccessToken: Token"],"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 { 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 } from '@utils/oAuth2';\nimport type { Types } from 'mongoose';\nimport type { Callback, Client } from 'oauth2-server';\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 // biome-ignore lint/correctness/noUnusedVariables: Just filter out clientId\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 * 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 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 { 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 } from '@utils/oAuth2';\nimport type { Types } from 'mongoose';\nimport type { Callback, Client } from 'oauth2-server';\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 // biome-ignore lint/correctness/noUnusedVariables: Just filter out clientId\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 * 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 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 +1 @@
1
- {"version":3,"file":"projectAccessKey.service.mjs","names":["newAccessKey: OAuth2AccessData"],"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,GAAG,CAAC,SAAS,MAAM;CACzC,cAAc,YAAY,GAAG,CAAC,SAAS,MAAM;CAC9C;;;;;;;;;;AAWD,MAAa,kBAAkB,OAC7B,eACA,WACA,SAC0B;CAC1B,MAAM,EAAE,UAAU,iBAAiB,2BAA2B;CAE9D,MAAMA,eAAiC;EACrC,GAAG;EACH;EACA;EACA,QAAQ,KAAK;EACb,aAAa,EAAE;EACf,QAAQ,cAAc;EACvB;AAOD,MALe,MAAM,aAAa,UAChC,EAAE,KAAK,WAAW,EAClB,EAAE,OAAO,EAAE,cAAc,cAAc,EAAE,CAC1C,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;EACd,CAAC;CAKJ,MAAM,kBAFiB,MAAM,eAAe,UAAU,EAEhB,aAAa,MAChD,WAAW,OAAO,aAAa,SACjC;AAED,KAAI,CAAC,eACH,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;EACd,CAAC;AAGJ,QAAO;;AAGT,MAAa,kBAAkB,OAC7B,UACA,SACA,WACG;CACH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WACC,OAAO,aAAa,YAAY,OAAO,OAAO,OAAO,KAAK,OAAO,OAAO,CAC3E;AAED,KAAI,CAAC,cACH,OAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;EACpB,CAAC;AAWJ,MARe,MAAM,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,OAAO;EACtC,EACD,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,EAAE,CAC1C,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA,WAAW,QAAQ;EACpB,CAAC;AAGJ,QAAO;;AAGT,MAAa,mBAAmB,OAC9B,UACA,WACA,WAC0B;CAC1B,MAAM,UAAU,MAAM,aAAa,QAAQ;EACzC,KAAK;EACL,yBAAyB;EACzB,uBAAuB,OAAO,OAAO;EACtC,CAAC;AAEF,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,qBAAqB;EAC1C;EACA;EACA;EACD,CAAC;CAGJ,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WAAW,OAAO,aAAa,SACjC;AAED,KAAI,CAAC,cACH,OAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;EACpB,CAAC;CAGJ,MAAM,EAAE,iBAAiB,2BAA2B;AAepD,MAbe,MAAM,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,OAAO;EACtC,EACD,EACE,MAAM;EACJ,2BAA2B,cAAc;EACzC,+BAA+B;EAChC,EACF,CACF,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,4BAA4B;EACjD;EACA;EACD,CAAC;CAGJ,MAAM,iBAAiB,MAAM,eAAe,UAAU;CAEtD,MAAM,iBAAiB,eAAe,aAAa,MAChD,WAAW,OAAO,aAAa,cAAc,SAC/C;AAED,KAAI,CAAC,eACH,OAAM,IAAI,aAAa,8BAA8B;EACnD,eAAe,eAAe;EAC9B;EACA;EACD,CAAC;AAGJ,QAAO"}
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,GAAG,CAAC,SAAS,MAAM;CACzC,cAAc,YAAY,GAAG,CAAC,SAAS,MAAM;CAC9C;;;;;;;;;;AAWD,MAAa,kBAAkB,OAC7B,eACA,WACA,SAC0B;CAC1B,MAAM,EAAE,UAAU,iBAAiB,2BAA2B;CAE9D,MAAM,eAAiC;EACrC,GAAG;EACH;EACA;EACA,QAAQ,KAAK;EACb,aAAa,EAAE;EACf,QAAQ,cAAc;EACvB;AAOD,MALe,MAAM,aAAa,UAChC,EAAE,KAAK,WAAW,EAClB,EAAE,OAAO,EAAE,cAAc,cAAc,EAAE,CAC1C,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;EACd,CAAC;CAKJ,MAAM,kBAFiB,MAAM,eAAe,UAAU,EAEhB,aAAa,MAChD,WAAW,OAAO,aAAa,SACjC;AAED,KAAI,CAAC,eACH,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;EACd,CAAC;AAGJ,QAAO;;AAGT,MAAa,kBAAkB,OAC7B,UACA,SACA,WACG;CACH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WACC,OAAO,aAAa,YAAY,OAAO,OAAO,OAAO,KAAK,OAAO,OAAO,CAC3E;AAED,KAAI,CAAC,cACH,OAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;EACpB,CAAC;AAWJ,MARe,MAAM,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,OAAO;EACtC,EACD,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,EAAE,CAC1C,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA,WAAW,QAAQ;EACpB,CAAC;AAGJ,QAAO;;AAGT,MAAa,mBAAmB,OAC9B,UACA,WACA,WAC0B;CAC1B,MAAM,UAAU,MAAM,aAAa,QAAQ;EACzC,KAAK;EACL,yBAAyB;EACzB,uBAAuB,OAAO,OAAO;EACtC,CAAC;AAEF,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,qBAAqB;EAC1C;EACA;EACA;EACD,CAAC;CAGJ,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WAAW,OAAO,aAAa,SACjC;AAED,KAAI,CAAC,cACH,OAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;EACpB,CAAC;CAGJ,MAAM,EAAE,iBAAiB,2BAA2B;AAepD,MAbe,MAAM,aAAa,UAChC;EACE,yBAAyB;EACzB,uBAAuB,OAAO,OAAO;EACtC,EACD,EACE,MAAM;EACJ,2BAA2B,cAAc;EACzC,+BAA+B;EAChC,EACF,CACF,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,4BAA4B;EACjD;EACA;EACD,CAAC;CAGJ,MAAM,iBAAiB,MAAM,eAAe,UAAU;CAEtD,MAAM,iBAAiB,eAAe,aAAa,MAChD,WAAW,OAAO,aAAa,cAAc,SAC/C;AAED,KAAI,CAAC,eACH,OAAM,IAAI,aAAa,8BAA8B;EACnD,eAAe,eAAe;EAC9B;EACA;EACD,CAAC;AAGJ,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.service.mjs","names":["Stripe","discountType: 'amount' | 'percentage' | null","results: PricingResult"],"sources":["../../../src/services/subscription.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { GenericError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\nimport { sendEmail } from './email.service';\nimport { getOrganizationById, updatePlan } from './organization.service';\nimport { getUserById } from './user.service';\n\nexport const addOrUpdateSubscription = async (\n subscriptionId: string,\n priceId: string,\n customerId: string,\n userId: string,\n organization: Organization,\n status: Plan['status']\n): Promise<Plan | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n });\n }\n\n if (String(user.customerId) !== customerId) {\n (user.customerId as unknown as string) = customerId;\n await user.save();\n }\n\n const planInfo = retrievePlanInformation(priceId);\n\n const subscriptions = await stripe.subscriptions.list({\n customer: customerId,\n status: 'active',\n limit: 1,\n });\n\n if (subscriptions.data.length >= 1) {\n // Active subscription exists; update it to the new plan\n const otherSubscriptionArray = subscriptions.data.filter(\n (subscription) => subscription.id !== subscriptionId\n );\n\n for (const subscription of otherSubscriptionArray) {\n await stripe.subscriptions.cancel(subscription.id);\n }\n }\n\n const updatedOrganization = await updatePlan(organization, {\n creatorId: user.id,\n priceId,\n customerId,\n subscriptionId,\n type: planInfo.type,\n period: planInfo.period,\n status,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Plan updated for organization ${organization.id} - ${planInfo.type} - ${planInfo.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const cancelSubscription = async (\n subscriptionId: string | Organization['id'],\n organizationId: Organization['id'] | string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n subscriptionId,\n });\n }\n\n if (!subscriptionId) {\n throw new GenericError('NO_SUBSCRIPTION_ID_PROVIDED');\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status: 'canceled',\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Cancelled plan for organization ${updatedOrganization.id} - ${updatedOrganization.plan?.type} - ${updatedOrganization.plan?.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const changeSubscriptionStatus = async (\n subscriptionId: string,\n status: Plan['status'],\n userId: string,\n organizationId: string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n userId,\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status,\n subscriptionId,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n logger.info(\n `Updated plan status for organization ${organization.id} - Status: ${status}`\n );\n\n const emailData = {\n to: user.email,\n username: user.name,\n email: user.email,\n planName: organization.plan.type,\n date: new Date().toLocaleDateString(),\n link: `${process.env.APP_URL}/dashboard`,\n };\n\n switch (status) {\n case 'active':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentSuccess',\n organizationName: organization.name,\n subscriptionStartDate: emailData.date,\n manageSubscriptionLink: emailData.link,\n });\n break;\n case 'canceled':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentCancellation',\n organizationName: organization.name,\n cancellationDate: emailData.date,\n reactivateLink: emailData.link,\n });\n break;\n case 'incomplete':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentError',\n organizationName: organization.name,\n errorDate: emailData.date,\n retryPaymentLink: emailData.link,\n });\n break;\n default:\n logger.warn(`Unhandled subscription status: ${status}`);\n }\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const getCouponId = async (\n promoCode: string\n): Promise<string | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Retrieve the coupon details by name\n const coupons = await stripe.coupons.list();\n const matchingCoupon = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n\n return matchingCoupon ? matchingCoupon.id : null;\n } catch (error) {\n console.error('Error retrieving coupon:', error);\n return null;\n }\n};\n\nexport type PricingResult = Record<\n string,\n {\n originalTotal: number;\n discountApplied: number;\n discountType: 'amount' | 'percentage' | null;\n finalTotal: number;\n currency: string;\n }\n>;\n\nexport const getPricing = async (\n priceIds: string[],\n promoCode?: string\n): Promise<PricingResult> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // 1. Fetch all price objects\n const pricePromises = priceIds.map((priceId) =>\n stripe.prices.retrieve(priceId)\n );\n const prices = await Promise.all(pricePromises);\n\n // Calculate the total amount before discount (to help with proportional distribution if needed)\n const totalAmount = prices.reduce(\n (sum, price) => sum + (price.unit_amount ?? 0),\n 0\n );\n\n // 2. Retrieve the discount (if promo code is provided)\n let discountAmount = 0;\n let discountType: 'amount' | 'percentage' | null = null;\n\n if (promoCode) {\n const coupons = await stripe.coupons.list();\n const matchingCoupons = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n if (matchingCoupons) {\n if (matchingCoupons.amount_off) {\n discountAmount = matchingCoupons.amount_off;\n discountType = 'amount';\n } else if (matchingCoupons.percent_off) {\n // For a percentage discount, we won't store discountAmount as a raw number\n // because each price line is discounted individually by the same percentage.\n discountAmount = matchingCoupons.percent_off;\n discountType = 'percentage';\n }\n }\n }\n\n // 3. Build the result for each priceId\n const results: PricingResult = {};\n\n for (const price of prices) {\n if (!price.id || !price.unit_amount) {\n continue; // Skip any invalid price\n }\n\n const originalTotal = price.unit_amount;\n let appliedDiscount = 0;\n let finalTotal = originalTotal;\n\n // Apply discount based on the discount type\n if (discountType === 'percentage' && discountAmount > 0) {\n // percentage-based discount\n appliedDiscount = (originalTotal * discountAmount) / 100;\n finalTotal = originalTotal - appliedDiscount;\n } else if (\n discountType === 'amount' &&\n totalAmount > 0 &&\n discountAmount > 0\n ) {\n // fixed amount discount - distribute proportionally\n const proportion = originalTotal / totalAmount;\n appliedDiscount = discountAmount * proportion;\n finalTotal = originalTotal - appliedDiscount;\n }\n\n // Prevent final total from going negative due to rounding\n finalTotal = Math.max(finalTotal, 0);\n\n results[price.id] = {\n originalTotal: originalTotal,\n discountApplied: appliedDiscount,\n discountType,\n finalTotal: finalTotal,\n currency: price.currency,\n };\n }\n\n return results;\n } catch (error) {\n console.error('Error calculating pricing per priceId:', error);\n throw new Error('Failed to calculate pricing breakdown.');\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscription.service.mjs","names":["Stripe"],"sources":["../../../src/services/subscription.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { GenericError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\nimport { sendEmail } from './email.service';\nimport { getOrganizationById, updatePlan } from './organization.service';\nimport { getUserById } from './user.service';\n\nexport const addOrUpdateSubscription = async (\n subscriptionId: string,\n priceId: string,\n customerId: string,\n userId: string,\n organization: Organization,\n status: Plan['status']\n): Promise<Plan | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n });\n }\n\n if (String(user.customerId) !== customerId) {\n (user.customerId as unknown as string) = customerId;\n await user.save();\n }\n\n const planInfo = retrievePlanInformation(priceId);\n\n const subscriptions = await stripe.subscriptions.list({\n customer: customerId,\n status: 'active',\n limit: 1,\n });\n\n if (subscriptions.data.length >= 1) {\n // Active subscription exists; update it to the new plan\n const otherSubscriptionArray = subscriptions.data.filter(\n (subscription) => subscription.id !== subscriptionId\n );\n\n for (const subscription of otherSubscriptionArray) {\n await stripe.subscriptions.cancel(subscription.id);\n }\n }\n\n const updatedOrganization = await updatePlan(organization, {\n creatorId: user.id,\n priceId,\n customerId,\n subscriptionId,\n type: planInfo.type,\n period: planInfo.period,\n status,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Plan updated for organization ${organization.id} - ${planInfo.type} - ${planInfo.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const cancelSubscription = async (\n subscriptionId: string | Organization['id'],\n organizationId: Organization['id'] | string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n subscriptionId,\n });\n }\n\n if (!subscriptionId) {\n throw new GenericError('NO_SUBSCRIPTION_ID_PROVIDED');\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status: 'canceled',\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Cancelled plan for organization ${updatedOrganization.id} - ${updatedOrganization.plan?.type} - ${updatedOrganization.plan?.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const changeSubscriptionStatus = async (\n subscriptionId: string,\n status: Plan['status'],\n userId: string,\n organizationId: string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n userId,\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status,\n subscriptionId,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n logger.info(\n `Updated plan status for organization ${organization.id} - Status: ${status}`\n );\n\n const emailData = {\n to: user.email,\n username: user.name,\n email: user.email,\n planName: organization.plan.type,\n date: new Date().toLocaleDateString(),\n link: `${process.env.APP_URL}/dashboard`,\n };\n\n switch (status) {\n case 'active':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentSuccess',\n organizationName: organization.name,\n subscriptionStartDate: emailData.date,\n manageSubscriptionLink: emailData.link,\n });\n break;\n case 'canceled':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentCancellation',\n organizationName: organization.name,\n cancellationDate: emailData.date,\n reactivateLink: emailData.link,\n });\n break;\n case 'incomplete':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentError',\n organizationName: organization.name,\n errorDate: emailData.date,\n retryPaymentLink: emailData.link,\n });\n break;\n default:\n logger.warn(`Unhandled subscription status: ${status}`);\n }\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const getCouponId = async (\n promoCode: string\n): Promise<string | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Retrieve the coupon details by name\n const coupons = await stripe.coupons.list();\n const matchingCoupon = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n\n return matchingCoupon ? matchingCoupon.id : null;\n } catch (error) {\n console.error('Error retrieving coupon:', error);\n return null;\n }\n};\n\nexport type PricingResult = Record<\n string,\n {\n originalTotal: number;\n discountApplied: number;\n discountType: 'amount' | 'percentage' | null;\n finalTotal: number;\n currency: string;\n }\n>;\n\nexport const getPricing = async (\n priceIds: string[],\n promoCode?: string\n): Promise<PricingResult> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // 1. Fetch all price objects\n const pricePromises = priceIds.map((priceId) =>\n stripe.prices.retrieve(priceId)\n );\n const prices = await Promise.all(pricePromises);\n\n // Calculate the total amount before discount (to help with proportional distribution if needed)\n const totalAmount = prices.reduce(\n (sum, price) => sum + (price.unit_amount ?? 0),\n 0\n );\n\n // 2. Retrieve the discount (if promo code is provided)\n let discountAmount = 0;\n let discountType: 'amount' | 'percentage' | null = null;\n\n if (promoCode) {\n const coupons = await stripe.coupons.list();\n const matchingCoupons = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n if (matchingCoupons) {\n if (matchingCoupons.amount_off) {\n discountAmount = matchingCoupons.amount_off;\n discountType = 'amount';\n } else if (matchingCoupons.percent_off) {\n // For a percentage discount, we won't store discountAmount as a raw number\n // because each price line is discounted individually by the same percentage.\n discountAmount = matchingCoupons.percent_off;\n discountType = 'percentage';\n }\n }\n }\n\n // 3. Build the result for each priceId\n const results: PricingResult = {};\n\n for (const price of prices) {\n if (!price.id || !price.unit_amount) {\n continue; // Skip any invalid price\n }\n\n const originalTotal = price.unit_amount;\n let appliedDiscount = 0;\n let finalTotal = originalTotal;\n\n // Apply discount based on the discount type\n if (discountType === 'percentage' && discountAmount > 0) {\n // percentage-based discount\n appliedDiscount = (originalTotal * discountAmount) / 100;\n finalTotal = originalTotal - appliedDiscount;\n } else if (\n discountType === 'amount' &&\n totalAmount > 0 &&\n discountAmount > 0\n ) {\n // fixed amount discount - distribute proportionally\n const proportion = originalTotal / totalAmount;\n appliedDiscount = discountAmount * proportion;\n finalTotal = originalTotal - appliedDiscount;\n }\n\n // Prevent final total from going negative due to rounding\n finalTotal = Math.max(finalTotal, 0);\n\n results[price.id] = {\n originalTotal: originalTotal,\n discountApplied: appliedDiscount,\n discountType,\n finalTotal: finalTotal,\n currency: price.currency,\n };\n }\n\n return results;\n } catch (error) {\n console.error('Error calculating pricing per priceId:', error);\n throw new Error('Failed to calculate pricing breakdown.');\n }\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"user.service.mjs","names":["newUser: UserDocument"],"sources":["../../../src/services/user.service.ts"],"sourcesContent":["import { UserModel } from '@models/user.model';\nimport { GenericError } from '@utils/errors';\nimport type { UserFilters } from '@utils/filtersAndPagination/getUserFiltersAndPagination';\nimport {\n type FieldsToCheck,\n type UserFields,\n validateUser,\n} from '@utils/validation/validateUser';\nimport type { Types } from 'mongoose';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\n\n/**\n * Creates a new user with password in the database and hashes the password.\n * @param user - User object with password not hashed.\n * @returns Created user object.\n */\nexport const createUser = async (\n user: Partial<User>\n): Promise<UserDocument> => {\n const fieldsToCheck: FieldsToCheck[] = ['email'];\n\n const errors = validateUser(user, fieldsToCheck);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userEmail: user.email,\n errors,\n });\n }\n\n const newUser: UserDocument = await UserModel.create(user);\n\n if (!newUser) {\n throw new GenericError('USER_CREATION_FAILED', { userEmail: user.email });\n }\n\n return newUser;\n};\n\n/**\n * Retrieves a user by email.\n * @param email - User's email.\n * @returns User object or null if no user was found.\n */\nexport const getUserByEmail = async (\n email: string\n): Promise<UserDocument | null> => {\n return await UserModel.findOne({ email });\n};\n\n/**\n * Retrieves users list by email.\n * @param emails - Users email.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByEmails = async (\n emails: string[]\n): Promise<UserDocument[] | null> => {\n return await UserModel.find({ email: { $in: emails } });\n};\n\n/**\n * Checks if a user exists by email.\n * @param email - User's email.\n * @returns True if the user exists, false otherwise.\n */\nexport const checkUserExists = async (email: string): Promise<boolean> => {\n const user = await UserModel.exists({ email });\n return user !== null;\n};\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUserById = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument | null> => await UserModel.findById(userId);\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByIds = async (\n userIds: (string | Types.ObjectId)[]\n): Promise<UserDocument[] | null> =>\n await UserModel.find({ _id: { $in: userIds } });\n\n/**\n * Finds users 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 * @returns List of users matching the filters.\n */\nexport const findUsers = async (\n filters: UserFilters,\n skip: number,\n limit: number,\n sortOptions?: Record<string, 1 | -1>\n): Promise<UserDocument[]> => {\n let query = UserModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Counts the total number of users that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of users.\n */\nexport const countUsers = async (filters: UserFilters): Promise<number> => {\n const count = await UserModel.countDocuments(filters);\n\n if (typeof count === 'undefined') {\n throw new GenericError('USER_COUNT_FAILED');\n }\n\n return count;\n};\n\n/**\n * Updates a user's information.\n * @param user - The user object.\n * @param updates - The updates to apply to the user.\n * @returns The updated user.\n */\nexport const updateUserById = async (\n userId: string | Types.ObjectId,\n updates: Partial<UserAPI>\n): Promise<UserDocument> => {\n const keyToValidate = Object.keys(updates) as UserFields;\n const errors = validateUser(updates, keyToValidate);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userId,\n errors,\n });\n }\n\n const result = await UserModel.updateOne({ _id: userId }, { $set: updates });\n\n if (result.matchedCount === 0) {\n throw new GenericError('USER_UPDATE_FAILED', { userId });\n }\n\n const updatedUser = await UserModel.findById(userId);\n\n if (!updatedUser) {\n throw new GenericError('USER_UPDATED_USER_NOT_FOUND', { userId });\n }\n\n return updatedUser;\n};\n\n/**\n * Deletes a user from the database.\n * @param userId - The user object.\n * @returns\n */\nexport const deleteUser = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument> => {\n await getUserById(userId);\n\n const user = await UserModel.findByIdAndDelete(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', { userId });\n }\n\n return user;\n};\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,aAAa,OACxB,SAC0B;CAG1B,MAAM,SAAS,aAAa,MAFW,CAAC,QAAQ,CAEA;AAEhD,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,uBAAuB;EAC5C,WAAW,KAAK;EAChB;EACD,CAAC;CAGJ,MAAMA,UAAwB,MAAM,UAAU,OAAO,KAAK;AAE1D,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,wBAAwB,EAAE,WAAW,KAAK,OAAO,CAAC;AAG3E,QAAO;;;;;;;AAQT,MAAa,iBAAiB,OAC5B,UACiC;AACjC,QAAO,MAAM,UAAU,QAAQ,EAAE,OAAO,CAAC;;;;;;;AAQ3C,MAAa,mBAAmB,OAC9B,WACmC;AACnC,QAAO,MAAM,UAAU,KAAK,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;;;;;;;AAQzD,MAAa,kBAAkB,OAAO,UAAoC;AAExE,QADa,MAAM,UAAU,OAAO,EAAE,OAAO,CAAC,KAC9B;;;;;;;AAQlB,MAAa,cAAc,OACzB,WACiC,MAAM,UAAU,SAAS,OAAO;;;;;;AAOnE,MAAa,gBAAgB,OAC3B,YAEA,MAAM,UAAU,KAAK,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,CAAC;;;;;;;;;AAUjD,MAAa,YAAY,OACvB,SACA,MACA,OACA,gBAC4B;CAC5B,IAAI,QAAQ,UAAU,KAAK,QAAQ,CAAC,KAAK,KAAK,CAAC,MAAM,MAAM;AAE3D,KAAI,eAAe,OAAO,KAAK,YAAY,CAAC,SAAS,EACnD,SAAQ,MAAM,KAAK,YAAY;AAGjC,QAAO,MAAM;;;;;;;AAQf,MAAa,aAAa,OAAO,YAA0C;CACzE,MAAM,QAAQ,MAAM,UAAU,eAAe,QAAQ;AAErD,KAAI,OAAO,UAAU,YACnB,OAAM,IAAI,aAAa,oBAAoB;AAG7C,QAAO;;;;;;;;AAST,MAAa,iBAAiB,OAC5B,QACA,YAC0B;CAE1B,MAAM,SAAS,aAAa,SADN,OAAO,KAAK,QAAQ,CACS;AAEnD,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,uBAAuB;EAC5C;EACA;EACD,CAAC;AAKJ,MAFe,MAAM,UAAU,UAAU,EAAE,KAAK,QAAQ,EAAE,EAAE,MAAM,SAAS,CAAC,EAEjE,iBAAiB,EAC1B,OAAM,IAAI,aAAa,sBAAsB,EAAE,QAAQ,CAAC;CAG1D,MAAM,cAAc,MAAM,UAAU,SAAS,OAAO;AAEpD,KAAI,CAAC,YACH,OAAM,IAAI,aAAa,+BAA+B,EAAE,QAAQ,CAAC;AAGnE,QAAO;;;;;;;AAQT,MAAa,aAAa,OACxB,WAC0B;AAC1B,OAAM,YAAY,OAAO;CAEzB,MAAM,OAAO,MAAM,UAAU,kBAAkB,OAAO;AAEtD,KAAI,CAAC,KACH,OAAM,IAAI,aAAa,kBAAkB,EAAE,QAAQ,CAAC;AAGtD,QAAO"}
1
+ {"version":3,"file":"user.service.mjs","names":[],"sources":["../../../src/services/user.service.ts"],"sourcesContent":["import { UserModel } from '@models/user.model';\nimport { GenericError } from '@utils/errors';\nimport type { UserFilters } from '@utils/filtersAndPagination/getUserFiltersAndPagination';\nimport {\n type FieldsToCheck,\n type UserFields,\n validateUser,\n} from '@utils/validation/validateUser';\nimport type { Types } from 'mongoose';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\n\n/**\n * Creates a new user with password in the database and hashes the password.\n * @param user - User object with password not hashed.\n * @returns Created user object.\n */\nexport const createUser = async (\n user: Partial<User>\n): Promise<UserDocument> => {\n const fieldsToCheck: FieldsToCheck[] = ['email'];\n\n const errors = validateUser(user, fieldsToCheck);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userEmail: user.email,\n errors,\n });\n }\n\n const newUser: UserDocument = await UserModel.create(user);\n\n if (!newUser) {\n throw new GenericError('USER_CREATION_FAILED', { userEmail: user.email });\n }\n\n return newUser;\n};\n\n/**\n * Retrieves a user by email.\n * @param email - User's email.\n * @returns User object or null if no user was found.\n */\nexport const getUserByEmail = async (\n email: string\n): Promise<UserDocument | null> => {\n return await UserModel.findOne({ email });\n};\n\n/**\n * Retrieves users list by email.\n * @param emails - Users email.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByEmails = async (\n emails: string[]\n): Promise<UserDocument[] | null> => {\n return await UserModel.find({ email: { $in: emails } });\n};\n\n/**\n * Checks if a user exists by email.\n * @param email - User's email.\n * @returns True if the user exists, false otherwise.\n */\nexport const checkUserExists = async (email: string): Promise<boolean> => {\n const user = await UserModel.exists({ email });\n return user !== null;\n};\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUserById = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument | null> => await UserModel.findById(userId);\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByIds = async (\n userIds: (string | Types.ObjectId)[]\n): Promise<UserDocument[] | null> =>\n await UserModel.find({ _id: { $in: userIds } });\n\n/**\n * Finds users 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 * @returns List of users matching the filters.\n */\nexport const findUsers = async (\n filters: UserFilters,\n skip: number,\n limit: number,\n sortOptions?: Record<string, 1 | -1>\n): Promise<UserDocument[]> => {\n let query = UserModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Counts the total number of users that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of users.\n */\nexport const countUsers = async (filters: UserFilters): Promise<number> => {\n const count = await UserModel.countDocuments(filters);\n\n if (typeof count === 'undefined') {\n throw new GenericError('USER_COUNT_FAILED');\n }\n\n return count;\n};\n\n/**\n * Updates a user's information.\n * @param user - The user object.\n * @param updates - The updates to apply to the user.\n * @returns The updated user.\n */\nexport const updateUserById = async (\n userId: string | Types.ObjectId,\n updates: Partial<UserAPI>\n): Promise<UserDocument> => {\n const keyToValidate = Object.keys(updates) as UserFields;\n const errors = validateUser(updates, keyToValidate);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userId,\n errors,\n });\n }\n\n const result = await UserModel.updateOne({ _id: userId }, { $set: updates });\n\n if (result.matchedCount === 0) {\n throw new GenericError('USER_UPDATE_FAILED', { userId });\n }\n\n const updatedUser = await UserModel.findById(userId);\n\n if (!updatedUser) {\n throw new GenericError('USER_UPDATED_USER_NOT_FOUND', { userId });\n }\n\n return updatedUser;\n};\n\n/**\n * Deletes a user from the database.\n * @param userId - The user object.\n * @returns\n */\nexport const deleteUser = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument> => {\n await getUserById(userId);\n\n const user = await UserModel.findByIdAndDelete(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', { userId });\n }\n\n return user;\n};\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,aAAa,OACxB,SAC0B;CAG1B,MAAM,SAAS,aAAa,MAFW,CAAC,QAAQ,CAEA;AAEhD,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,uBAAuB;EAC5C,WAAW,KAAK;EAChB;EACD,CAAC;CAGJ,MAAM,UAAwB,MAAM,UAAU,OAAO,KAAK;AAE1D,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,wBAAwB,EAAE,WAAW,KAAK,OAAO,CAAC;AAG3E,QAAO;;;;;;;AAQT,MAAa,iBAAiB,OAC5B,UACiC;AACjC,QAAO,MAAM,UAAU,QAAQ,EAAE,OAAO,CAAC;;;;;;;AAQ3C,MAAa,mBAAmB,OAC9B,WACmC;AACnC,QAAO,MAAM,UAAU,KAAK,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;;;;;;;AAQzD,MAAa,kBAAkB,OAAO,UAAoC;AAExE,QADa,MAAM,UAAU,OAAO,EAAE,OAAO,CAAC,KAC9B;;;;;;;AAQlB,MAAa,cAAc,OACzB,WACiC,MAAM,UAAU,SAAS,OAAO;;;;;;AAOnE,MAAa,gBAAgB,OAC3B,YAEA,MAAM,UAAU,KAAK,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,CAAC;;;;;;;;;AAUjD,MAAa,YAAY,OACvB,SACA,MACA,OACA,gBAC4B;CAC5B,IAAI,QAAQ,UAAU,KAAK,QAAQ,CAAC,KAAK,KAAK,CAAC,MAAM,MAAM;AAE3D,KAAI,eAAe,OAAO,KAAK,YAAY,CAAC,SAAS,EACnD,SAAQ,MAAM,KAAK,YAAY;AAGjC,QAAO,MAAM;;;;;;;AAQf,MAAa,aAAa,OAAO,YAA0C;CACzE,MAAM,QAAQ,MAAM,UAAU,eAAe,QAAQ;AAErD,KAAI,OAAO,UAAU,YACnB,OAAM,IAAI,aAAa,oBAAoB;AAG7C,QAAO;;;;;;;;AAST,MAAa,iBAAiB,OAC5B,QACA,YAC0B;CAE1B,MAAM,SAAS,aAAa,SADN,OAAO,KAAK,QAAQ,CACS;AAEnD,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,uBAAuB;EAC5C;EACA;EACD,CAAC;AAKJ,MAFe,MAAM,UAAU,UAAU,EAAE,KAAK,QAAQ,EAAE,EAAE,MAAM,SAAS,CAAC,EAEjE,iBAAiB,EAC1B,OAAM,IAAI,aAAa,sBAAsB,EAAE,QAAQ,CAAC;CAG1D,MAAM,cAAc,MAAM,UAAU,SAAS,OAAO;AAEpD,KAAI,CAAC,YACH,OAAM,IAAI,aAAa,+BAA+B,EAAE,QAAQ,CAAC;AAGnE,QAAO;;;;;;;AAQT,MAAa,aAAa,OACxB,WAC0B;AAC1B,OAAM,YAAY,OAAO;CAEzB,MAAM,OAAO,MAAM,UAAU,kBAAkB,OAAO;AAEtD,KAAI,CAAC,KACH,OAAM,IAAI,aAAa,kBAAkB,EAAE,QAAQ,CAAC;AAGtD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.service.mjs","names":["results: TriggerResult[]","error: any","headers: Record<string, string>"],"sources":["../../../src/services/webhook.service.ts"],"sourcesContent":["import { createHmac } from 'node:crypto';\nimport { logger } from '@logger';\nimport { Octokit } from '@octokit/rest';\nimport type { Project } from '@/types/project.types';\n\nexport type TriggerResult = {\n target: string;\n success: boolean;\n message?: string;\n};\n\n/**\n * Main entry point to trigger all configured CI pipelines for a project\n */\nexport const triggerAll = async (\n project: Project\n): Promise<TriggerResult[]> => {\n const results: TriggerResult[] = [];\n\n // Trigger Git Provider Pipeline (if configured)\n if (project.repository && project.webhooks?.autoTriggerBuilds) {\n try {\n await triggerGitPipeline(project);\n results.push({\n target: project.repository.provider,\n success: true,\n });\n } catch (error: any) {\n logger.error(`Failed to trigger ${project.repository.provider}`, error);\n results.push({\n target: project.repository.provider,\n success: false,\n message: error.message || String(error),\n });\n }\n }\n\n // Trigger Generic Webhooks (Vercel, etc.)\n const webhooks = project.webhooks?.webhooks || [];\n\n // Using Promise.all is often better here, but keeping your sequential loop logic for safety\n for (const hook of webhooks) {\n if (!hook.enabled) continue;\n try {\n await triggerGenericWebhook(hook);\n results.push({ target: hook.name, success: true });\n } catch (error: any) {\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n results.push({\n target: hook.name,\n success: false,\n message: error.message || String(error),\n });\n }\n }\n\n return results;\n};\n\n/**\n * Triggers a single webhook by index\n */\nexport const triggerSingleWebhook = async (\n project: Project,\n webhookIndex: number\n): Promise<TriggerResult> => {\n const webhooks = project.webhooks?.webhooks || [];\n\n if (webhookIndex < 0 || webhookIndex >= webhooks.length) {\n throw new Error(`Webhook index ${webhookIndex} is out of range`);\n }\n\n const hook = webhooks[webhookIndex];\n\n if (!hook.enabled) {\n throw new Error(`Webhook \"${hook.name}\" is disabled`);\n }\n\n try {\n await triggerGenericWebhook(hook);\n return { target: hook.name, success: true };\n } catch (error: any) {\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n return {\n target: hook.name,\n success: false,\n message: error.message || String(error),\n };\n }\n};\n\n// Internal Helper Functions (equivalent to private static methods)\n\nconst triggerGitPipeline = async (project: Project) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository) throw new Error('No repository configured');\n\n const token = oAuth2Access?.[0]?.accessToken?.[0]; // Get the first valid token\n\n if (!token) throw new Error('No valid OAuth token found');\n\n const { provider } = repository;\n\n switch (provider) {\n case 'github':\n return triggerGithub(repository, token);\n case 'gitlab':\n return triggerGitlab(repository, token);\n case 'bitbucket':\n return triggerBitbucket(repository, token);\n default:\n throw new Error(`Unknown provider: ${provider as string}`);\n }\n};\n\nconst triggerGithub = async (repo: any, token: string) => {\n const octokit = new Octokit({ auth: token });\n\n // Triggers a 'repository_dispatch' event\n // Workflow must listen to: types: [intlayer_cms_update]\n await octokit.repos.createDispatchEvent({\n owner: repo.owner,\n repo: repo.repository,\n event_type: 'intlayer_cms_update',\n client_payload: {\n timestamp: new Date().toISOString(),\n source: 'intlayer-cms',\n },\n });\n\n logger.info(\n `Successfully triggered GitHub Action for ${repo.owner}/${repo.repository}`\n );\n};\n\n// GitLab\nconst triggerGitlab = async (repo: any, token: string) => {\n // GitLab needs Project ID (int) or URL-encoded path \"owner/repo\"\n const projectId = encodeURIComponent(`${repo.owner}/${repo.repository}`);\n const branch = repo.branch || 'main';\n const baseUrl = repo.instanceUrl || 'https://gitlab.com';\n\n const url = `${baseUrl}/api/v4/projects/${projectId}/trigger/pipeline`;\n\n const formData = new FormData();\n formData.append('token', token); // Or a specific trigger token if stored separately\n formData.append('ref', branch);\n formData.append('variables[INTLAYER_UPDATE]', 'true');\n\n const res = await fetch(url, { method: 'POST', body: formData });\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`GitLab error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitLab pipeline for ${repo.owner}/${repo.repository}`\n );\n};\n\n// Bitbucket\nconst triggerBitbucket = async (repo: any, token: string) => {\n const workspace = repo.workspace || repo.owner; // Bitbucket uses 'workspace'\n const branch = repo.branch || 'main';\n const url = `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo.repository}/pipelines/`;\n\n const body = {\n target: {\n ref_type: 'branch',\n type: 'pipeline_ref_target',\n ref_name: branch,\n // Optional: Target a custom pipeline for security\n // selector: { type: 'custom', pattern: 'intlayer-update' }\n },\n variables: [{ key: 'INTLAYER_UPDATE', value: 'true', secured: false }],\n };\n\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`Bitbucket error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered Bitbucket pipeline for ${workspace}/${repo.repository}`\n );\n};\n\n// Generic Webhook\nconst triggerGenericWebhook = async (hook: any) => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add secret signature if provided (for webhook verification)\n if (hook.secret) {\n // Simple HMAC-SHA256 signature (can be enhanced)\n const payload = JSON.stringify({ event: 'intlayer_cms_update' });\n const signature = createHmac('sha256', hook.secret)\n .update(payload)\n .digest('hex');\n headers['X-Intlayer-Signature'] = signature;\n }\n\n const res = await fetch(hook.url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n event: 'intlayer_cms_update',\n timestamp: new Date().toISOString(),\n }),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(\n `Webhook ${hook.name} failed: ${res.status} - ${errorText}`\n );\n }\n\n logger.info(`Successfully triggered webhook: ${hook.name}`);\n};\n"],"mappings":";;;;;;;;AAcA,MAAa,aAAa,OACxB,YAC6B;CAC7B,MAAMA,UAA2B,EAAE;AAGnC,KAAI,QAAQ,cAAc,QAAQ,UAAU,kBAC1C,KAAI;AACF,QAAM,mBAAmB,QAAQ;AACjC,UAAQ,KAAK;GACX,QAAQ,QAAQ,WAAW;GAC3B,SAAS;GACV,CAAC;UACKC,OAAY;AACnB,SAAO,MAAM,qBAAqB,QAAQ,WAAW,YAAY,MAAM;AACvE,UAAQ,KAAK;GACX,QAAQ,QAAQ,WAAW;GAC3B,SAAS;GACT,SAAS,MAAM,WAAW,OAAO,MAAM;GACxC,CAAC;;CAKN,MAAM,WAAW,QAAQ,UAAU,YAAY,EAAE;AAGjD,MAAK,MAAM,QAAQ,UAAU;AAC3B,MAAI,CAAC,KAAK,QAAS;AACnB,MAAI;AACF,SAAM,sBAAsB,KAAK;AACjC,WAAQ,KAAK;IAAE,QAAQ,KAAK;IAAM,SAAS;IAAM,CAAC;WAC3CA,OAAY;AACnB,UAAO,MAAM,6BAA6B,KAAK,QAAQ,MAAM;AAC7D,WAAQ,KAAK;IACX,QAAQ,KAAK;IACb,SAAS;IACT,SAAS,MAAM,WAAW,OAAO,MAAM;IACxC,CAAC;;;AAIN,QAAO;;;;;AAMT,MAAa,uBAAuB,OAClC,SACA,iBAC2B;CAC3B,MAAM,WAAW,QAAQ,UAAU,YAAY,EAAE;AAEjD,KAAI,eAAe,KAAK,gBAAgB,SAAS,OAC/C,OAAM,IAAI,MAAM,iBAAiB,aAAa,kBAAkB;CAGlE,MAAM,OAAO,SAAS;AAEtB,KAAI,CAAC,KAAK,QACR,OAAM,IAAI,MAAM,YAAY,KAAK,KAAK,eAAe;AAGvD,KAAI;AACF,QAAM,sBAAsB,KAAK;AACjC,SAAO;GAAE,QAAQ,KAAK;GAAM,SAAS;GAAM;UACpCA,OAAY;AACnB,SAAO,MAAM,6BAA6B,KAAK,QAAQ,MAAM;AAC7D,SAAO;GACL,QAAQ,KAAK;GACb,SAAS;GACT,SAAS,MAAM,WAAW,OAAO,MAAM;GACxC;;;AAML,MAAM,qBAAqB,OAAO,YAAqB;CACrD,MAAM,EAAE,YAAY,iBAAiB;AAErC,KAAI,CAAC,WAAY,OAAM,IAAI,MAAM,2BAA2B;CAE5D,MAAM,QAAQ,eAAe,IAAI,cAAc;AAE/C,KAAI,CAAC,MAAO,OAAM,IAAI,MAAM,6BAA6B;CAEzD,MAAM,EAAE,aAAa;AAErB,SAAQ,UAAR;EACE,KAAK,SACH,QAAO,cAAc,YAAY,MAAM;EACzC,KAAK,SACH,QAAO,cAAc,YAAY,MAAM;EACzC,KAAK,YACH,QAAO,iBAAiB,YAAY,MAAM;EAC5C,QACE,OAAM,IAAI,MAAM,qBAAqB,WAAqB;;;AAIhE,MAAM,gBAAgB,OAAO,MAAW,UAAkB;AAKxD,OAJgB,IAAI,QAAQ,EAAE,MAAM,OAAO,CAAC,CAI9B,MAAM,oBAAoB;EACtC,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,YAAY;EACZ,gBAAgB;GACd,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,QAAQ;GACT;EACF,CAAC;AAEF,QAAO,KACL,4CAA4C,KAAK,MAAM,GAAG,KAAK,aAChE;;AAIH,MAAM,gBAAgB,OAAO,MAAW,UAAkB;CAExD,MAAM,YAAY,mBAAmB,GAAG,KAAK,MAAM,GAAG,KAAK,aAAa;CACxE,MAAM,SAAS,KAAK,UAAU;CAG9B,MAAM,MAAM,GAFI,KAAK,eAAe,qBAEb,mBAAmB,UAAU;CAEpD,MAAM,WAAW,IAAI,UAAU;AAC/B,UAAS,OAAO,SAAS,MAAM;AAC/B,UAAS,OAAO,OAAO,OAAO;AAC9B,UAAS,OAAO,8BAA8B,OAAO;CAErD,MAAM,MAAM,MAAM,MAAM,KAAK;EAAE,QAAQ;EAAQ,MAAM;EAAU,CAAC;AAChE,KAAI,CAAC,IAAI,IAAI;EACX,MAAM,YAAY,MAAM,IAAI,MAAM;AAClC,QAAM,IAAI,MAAM,iBAAiB,IAAI,OAAO,KAAK,YAAY;;AAG/D,QAAO,KACL,8CAA8C,KAAK,MAAM,GAAG,KAAK,aAClE;;AAIH,MAAM,mBAAmB,OAAO,MAAW,UAAkB;CAC3D,MAAM,YAAY,KAAK,aAAa,KAAK;CACzC,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,MAAM,8CAA8C,UAAU,GAAG,KAAK,WAAW;CAEvF,MAAM,OAAO;EACX,QAAQ;GACN,UAAU;GACV,MAAM;GACN,UAAU;GAGX;EACD,WAAW,CAAC;GAAE,KAAK;GAAmB,OAAO;GAAQ,SAAS;GAAO,CAAC;EACvE;CAED,MAAM,MAAM,MAAM,MAAM,KAAK;EAC3B,QAAQ;EACR,SAAS;GACP,eAAe,UAAU;GACzB,gBAAgB;GACjB;EACD,MAAM,KAAK,UAAU,KAAK;EAC3B,CAAC;AAEF,KAAI,CAAC,IAAI,IAAI;EACX,MAAM,YAAY,MAAM,IAAI,MAAM;AAClC,QAAM,IAAI,MAAM,oBAAoB,IAAI,OAAO,KAAK,YAAY;;AAGlE,QAAO,KACL,iDAAiD,UAAU,GAAG,KAAK,aACpE;;AAIH,MAAM,wBAAwB,OAAO,SAAc;CACjD,MAAMC,UAAkC,EACtC,gBAAgB,oBACjB;AAGD,KAAI,KAAK,QAAQ;EAEf,MAAM,UAAU,KAAK,UAAU,EAAE,OAAO,uBAAuB,CAAC;AAIhE,UAAQ,0BAHU,WAAW,UAAU,KAAK,OAAO,CAChD,OAAO,QAAQ,CACf,OAAO,MAAM;;CAIlB,MAAM,MAAM,MAAM,MAAM,KAAK,KAAK;EAChC,QAAQ;EACR;EACA,MAAM,KAAK,UAAU;GACnB,OAAO;GACP,4BAAW,IAAI,MAAM,EAAC,aAAa;GACpC,CAAC;EACH,CAAC;AAEF,KAAI,CAAC,IAAI,IAAI;EACX,MAAM,YAAY,MAAM,IAAI,MAAM;AAClC,QAAM,IAAI,MACR,WAAW,KAAK,KAAK,WAAW,IAAI,OAAO,KAAK,YACjD;;AAGH,QAAO,KAAK,mCAAmC,KAAK,OAAO"}
1
+ {"version":3,"file":"webhook.service.mjs","names":[],"sources":["../../../src/services/webhook.service.ts"],"sourcesContent":["import { createHmac } from 'node:crypto';\nimport { logger } from '@logger';\nimport { Octokit } from '@octokit/rest';\nimport type { Project } from '@/types/project.types';\n\nexport type TriggerResult = {\n target: string;\n success: boolean;\n message?: string;\n};\n\n/**\n * Main entry point to trigger all configured CI pipelines for a project\n */\nexport const triggerAll = async (\n project: Project\n): Promise<TriggerResult[]> => {\n const results: TriggerResult[] = [];\n\n // Trigger Git Provider Pipeline (if configured)\n if (project.repository && project.webhooks?.autoTriggerBuilds) {\n try {\n await triggerGitPipeline(project);\n results.push({\n target: project.repository.provider,\n success: true,\n });\n } catch (error: any) {\n logger.error(`Failed to trigger ${project.repository.provider}`, error);\n results.push({\n target: project.repository.provider,\n success: false,\n message: error.message || String(error),\n });\n }\n }\n\n // Trigger Generic Webhooks (Vercel, etc.)\n const webhooks = project.webhooks?.webhooks || [];\n\n // Using Promise.all is often better here, but keeping your sequential loop logic for safety\n for (const hook of webhooks) {\n if (!hook.enabled) continue;\n try {\n await triggerGenericWebhook(hook);\n results.push({ target: hook.name, success: true });\n } catch (error: any) {\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n results.push({\n target: hook.name,\n success: false,\n message: error.message || String(error),\n });\n }\n }\n\n return results;\n};\n\n/**\n * Triggers a single webhook by index\n */\nexport const triggerSingleWebhook = async (\n project: Project,\n webhookIndex: number\n): Promise<TriggerResult> => {\n const webhooks = project.webhooks?.webhooks || [];\n\n if (webhookIndex < 0 || webhookIndex >= webhooks.length) {\n throw new Error(`Webhook index ${webhookIndex} is out of range`);\n }\n\n const hook = webhooks[webhookIndex];\n\n if (!hook.enabled) {\n throw new Error(`Webhook \"${hook.name}\" is disabled`);\n }\n\n try {\n await triggerGenericWebhook(hook);\n return { target: hook.name, success: true };\n } catch (error: any) {\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n return {\n target: hook.name,\n success: false,\n message: error.message || String(error),\n };\n }\n};\n\n// Internal Helper Functions (equivalent to private static methods)\n\nconst triggerGitPipeline = async (project: Project) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository) throw new Error('No repository configured');\n\n const token = oAuth2Access?.[0]?.accessToken?.[0]; // Get the first valid token\n\n if (!token) throw new Error('No valid OAuth token found');\n\n const { provider } = repository;\n\n switch (provider) {\n case 'github':\n return triggerGithub(repository, token);\n case 'gitlab':\n return triggerGitlab(repository, token);\n case 'bitbucket':\n return triggerBitbucket(repository, token);\n default:\n throw new Error(`Unknown provider: ${provider as string}`);\n }\n};\n\nconst triggerGithub = async (repo: any, token: string) => {\n const octokit = new Octokit({ auth: token });\n\n // Triggers a 'repository_dispatch' event\n // Workflow must listen to: types: [intlayer_cms_update]\n await octokit.repos.createDispatchEvent({\n owner: repo.owner,\n repo: repo.repository,\n event_type: 'intlayer_cms_update',\n client_payload: {\n timestamp: new Date().toISOString(),\n source: 'intlayer-cms',\n },\n });\n\n logger.info(\n `Successfully triggered GitHub Action for ${repo.owner}/${repo.repository}`\n );\n};\n\n// GitLab\nconst triggerGitlab = async (repo: any, token: string) => {\n // GitLab needs Project ID (int) or URL-encoded path \"owner/repo\"\n const projectId = encodeURIComponent(`${repo.owner}/${repo.repository}`);\n const branch = repo.branch || 'main';\n const baseUrl = repo.instanceUrl || 'https://gitlab.com';\n\n const url = `${baseUrl}/api/v4/projects/${projectId}/trigger/pipeline`;\n\n const formData = new FormData();\n formData.append('token', token); // Or a specific trigger token if stored separately\n formData.append('ref', branch);\n formData.append('variables[INTLAYER_UPDATE]', 'true');\n\n const res = await fetch(url, { method: 'POST', body: formData });\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`GitLab error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitLab pipeline for ${repo.owner}/${repo.repository}`\n );\n};\n\n// Bitbucket\nconst triggerBitbucket = async (repo: any, token: string) => {\n const workspace = repo.workspace || repo.owner; // Bitbucket uses 'workspace'\n const branch = repo.branch || 'main';\n const url = `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo.repository}/pipelines/`;\n\n const body = {\n target: {\n ref_type: 'branch',\n type: 'pipeline_ref_target',\n ref_name: branch,\n // Optional: Target a custom pipeline for security\n // selector: { type: 'custom', pattern: 'intlayer-update' }\n },\n variables: [{ key: 'INTLAYER_UPDATE', value: 'true', secured: false }],\n };\n\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`Bitbucket error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered Bitbucket pipeline for ${workspace}/${repo.repository}`\n );\n};\n\n// Generic Webhook\nconst triggerGenericWebhook = async (hook: any) => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add secret signature if provided (for webhook verification)\n if (hook.secret) {\n // Simple HMAC-SHA256 signature (can be enhanced)\n const payload = JSON.stringify({ event: 'intlayer_cms_update' });\n const signature = createHmac('sha256', hook.secret)\n .update(payload)\n .digest('hex');\n headers['X-Intlayer-Signature'] = signature;\n }\n\n const res = await fetch(hook.url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n event: 'intlayer_cms_update',\n timestamp: new Date().toISOString(),\n }),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(\n `Webhook ${hook.name} failed: ${res.status} - ${errorText}`\n );\n }\n\n logger.info(`Successfully triggered webhook: ${hook.name}`);\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"askDocQuestion.mjs","names":["vectorStore: VectorStoreEl[]","MAX_RELEVANT_CHUNKS_NB: number","MIN_RELEVANT_CHUNKS_SIMILARITY: number","EMBEDDING_MODEL: OpenAI.EmbeddingModel","OVERLAP_TOKENS: number","MAX_CHUNK_TOKENS: number","CHAR_BY_TOKEN: number","MAX_CHARS: number","OVERLAP_CHARS: number","chunks: string[]","resultForFile: Record<string, number[] | undefined>","initPrompt: ChatCompletionRequestMessage"],"sources":["../../../../../src/utils/AI/askDocQuestion/askDocQuestion.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n type AIConfig,\n type ChatCompletionRequestMessage,\n streamText,\n} from '@intlayer/ai';\nimport { getMarkdownMetadata } from '@intlayer/core';\nimport { getBlogs, getDocs, getFrequentQuestions } from '@intlayer/docs';\nimport { OpenAI } from 'openai';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst readEmbeddingsForFile = (fileKey: string): Record<string, number[]> => {\n try {\n return JSON.parse(\n readFileSync(\n join(__dirname, `./embeddings/${fileKey.replace('.md', '.json')}`),\n 'utf-8'\n )\n ) as Record<string, number[]>;\n } catch {\n return {};\n }\n};\n\ntype VectorStoreEl = {\n fileKey: string;\n chunkNumber: number;\n content: string;\n embedding?: number[];\n docUrl: string;\n docName: string;\n};\n\n/**\n * Simple in-memory vector store to hold document embeddings and their content.\n * Each entry contains:\n * - fileKey: A unique key identifying the file\n * - chunkNumber: The number of the chunk within the document\n * - content: The chunk content\n * - embedding: The numerical embedding vector for the chunk\n */\nconst vectorStore: VectorStoreEl[] = [];\n\n/*\n * Ask question AI configuration\n */\nconst MAX_RELEVANT_CHUNKS_NB: number = 20; // Maximum number of relevant chunks to attach to chatGPT context\nconst MIN_RELEVANT_CHUNKS_SIMILARITY: number = 0.42; // Minimum similarity required for a chunk to be considered relevant\n\n/*\n * Embedding model configuration\n */\nconst EMBEDDING_MODEL: OpenAI.EmbeddingModel = 'text-embedding-3-large'; // Model to use for embedding generation\nconst OVERLAP_TOKENS: number = 200; // Number of tokens to overlap between chunks\nconst MAX_CHUNK_TOKENS: number = 800; // Maximum number of tokens per chunk\nconst CHAR_BY_TOKEN: number = 4.15; // Approximate pessimistically the number of characters per token // Can use `tiktoken` or other tokenizers to calculate it more precisely\nconst MAX_CHARS: number = MAX_CHUNK_TOKENS * CHAR_BY_TOKEN;\nconst OVERLAP_CHARS: number = OVERLAP_TOKENS * CHAR_BY_TOKEN;\n\nconst skipDocEmbeddingsIndex = process.env.SKIP_DOC_EMBEDDINGS_INDEX === 'true';\n\n/**\n * Splits a given text into chunks ensuring each chunk does not exceed MAX_CHARS.\n * @param text - The input text to split.\n * @returns - Array of text chunks.\n */\nconst chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let start = 0;\n\n while (start < text.length) {\n let end = Math.min(start + MAX_CHARS, text.length);\n\n // Ensure we don't cut words in the middle (find nearest space)\n if (end < text.length) {\n const lastSpace = text.lastIndexOf(' ', end);\n if (lastSpace > start) {\n end = lastSpace;\n }\n }\n\n chunks.push(text.substring(start, end));\n\n // Move start forward correctly\n const nextStart = end - OVERLAP_CHARS;\n if (nextStart <= start) {\n // Prevent infinite loop if overlap is too large\n start = end;\n } else {\n start = nextStart;\n }\n }\n\n return chunks;\n};\n\n/**\n * Generates an embedding for a given text using OpenAI's embedding API.\n *\n * @param text - The input text to generate an embedding for\n * @returns The embedding vector as a number array\n */\nconst generateEmbedding = async (text: string): Promise<number[]> => {\n // No try/catch here. If this fails, the controller should handle it.\n const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n const response = await openaiClient.embeddings.create({\n model: EMBEDDING_MODEL,\n input: text,\n });\n\n return response.data[0].embedding;\n};\n\n/**\n * Calculates the cosine similarity between two vectors.\n * Cosine similarity measures the cosine of the angle between two vectors in an inner product space.\n * Used to determine the similarity between chunks of text.\n *\n * @param vecA - The first vector\n * @param vecB - The second vector\n * @returns The cosine similarity score\n */\nconst cosineSimilarity = (vecA: number[], vecB: number[]): number => {\n // Calculate the dot product of the two vectors\n const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);\n\n // Calculate the magnitude (Euclidean norm) of each vector\n const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));\n const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));\n\n // Compute and return the cosine similarity\n return dotProduct / (magnitudeA * magnitudeB);\n};\n\n/**\n * Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.\n * Persists per-document embeddings under `embeddings/<fileKey>.json`.\n * Handles cases where files have been updated and chunk counts have changed.\n */\nexport const loadMarkdownFiles = async (): Promise<void> => {\n // Retrieve documentation and blog posts in English locale\n const frequentQuestions = await getFrequentQuestions();\n const docs = await getDocs();\n const blogs = await getBlogs();\n\n const files = { ...docs, ...blogs, ...frequentQuestions }; // Combine docs and blogs into a single object\n\n // Iterate over each file key (identifier) in the combined files\n for await (const fileKey of Object.keys(files)) {\n // Get the metadata of the file\n const fileMetadata = getMarkdownMetadata(\n files[fileKey as keyof typeof files] as string\n );\n\n // Split the document into chunks based on headings\n const fileChunks = chunkText(\n files[fileKey as keyof typeof files] as string\n );\n\n // Read existing embeddings for this file\n const existingEmbeddings = readEmbeddingsForFile(fileKey);\n\n // Check if the number of chunks has changed for this file\n const existingChunksForFile = Object.keys(existingEmbeddings);\n const currentChunkCount = fileChunks.length;\n const previousChunkCount = existingChunksForFile.length;\n\n let shouldRegenerateFileEmbeddings = false;\n\n // If chunk count differs, we need to regenerate embeddings for this file\n if (currentChunkCount !== previousChunkCount) {\n console.info(\n `File \"${fileKey}\" chunk count changed: ${previousChunkCount} -> ${currentChunkCount}. Regenerating embeddings.`\n );\n\n shouldRegenerateFileEmbeddings = !skipDocEmbeddingsIndex;\n }\n\n // Iterate over each chunk within the current file\n let resultForFile: Record<string, number[] | undefined> = {};\n for await (const chunkIndex of Object.keys(fileChunks)) {\n const chunkNumber = Number(chunkIndex) + 1; // Chunk number starts at 1\n const chunksNumber = fileChunks.length;\n\n const fileChunk = fileChunks[\n chunkIndex as keyof typeof fileChunks\n ] as string;\n\n const chunkKeyName = `chunk_${chunkNumber}`; // Unique key for the chunk within the file\n\n // Retrieve precomputed embedding if available and file hasn't changed\n const docEmbedding = !shouldRegenerateFileEmbeddings\n ? (existingEmbeddings[\n chunkKeyName as keyof typeof existingEmbeddings\n ] as number[] | undefined)\n : undefined;\n\n const embedding = docEmbedding; // Use existing embedding if available and valid\n\n // Update the file-scoped result object with the embedding\n resultForFile = { ...resultForFile, [chunkKeyName]: embedding };\n\n // Store the embedding and content in the in-memory vector store\n vectorStore.push({\n fileKey,\n chunkNumber,\n embedding,\n content: fileChunk,\n docUrl: fileMetadata.url,\n docName: fileMetadata.title,\n });\n\n console.info(`- Loaded: ${fileKey}/${chunkKeyName}/${chunksNumber}`);\n }\n }\n};\n\n// Automatically index Markdown files\nloadMarkdownFiles();\n\n/**\n * Searches the indexed documents for the most relevant chunks based on a query.\n * Utilizes cosine similarity to find the closest matching embeddings.\n *\n * @param query - The search query provided by the user\n * @returns An array of the top matching document chunks' content\n */\nexport const searchChunkReference = async (\n query: string,\n maxResults: number = MAX_RELEVANT_CHUNKS_NB,\n minSimilarity: number = MIN_RELEVANT_CHUNKS_SIMILARITY\n): Promise<VectorStoreEl[]> => {\n // Generate an embedding for the user's query\n const queryEmbedding = await generateEmbedding(query);\n\n // Calculate similarity scores between the query embedding and each document's embedding\n const selection = vectorStore\n .filter((chunk) => chunk.embedding)\n .map((chunk) => ({\n ...chunk,\n similarity: cosineSimilarity(queryEmbedding, chunk.embedding!), // Add similarity score to each doc\n }))\n .filter((chunk) => chunk.similarity > minSimilarity) // Filter out documents with low similarity scores\n .sort((a, b) => b.similarity - a.similarity) // Sort documents by highest similarity first\n .slice(0, maxResults); // Select the top 6 most similar documents\n\n const orderedDocKeys = new Set(selection.map((chunk) => chunk.fileKey));\n\n const orderedVectorStore = vectorStore.sort((a, _b) =>\n orderedDocKeys.has(a.fileKey) ? -1 : 1\n );\n\n const results = orderedVectorStore.filter((chunk) =>\n selection.some(\n (v) => v.fileKey === chunk.fileKey && v.chunkNumber === chunk.chunkNumber\n )\n );\n\n // Return the content of the top matching documents\n return results;\n};\n\nconst CHAT_GPT_PROMPT = readFileSync(join(__dirname, './PROMPT.md'), 'utf-8');\n\n// Initial prompt configuration for the chatbot\nexport const initPrompt: ChatCompletionRequestMessage = {\n role: 'system',\n content: CHAT_GPT_PROMPT,\n};\n\nexport type AskDocQuestionResult = {\n response: string;\n relatedFiles: string[];\n};\n\nexport type AskDocQuestionOptions = {\n onMessage?: (chunk: string) => void;\n};\n\n/**\n * Handles the \"Ask a question\" endpoint in an Express.js route.\n * Processes user messages, retrieves relevant documents, and interacts with AI models to generate responses.\n *\n * @param messages - An array of chat messages from the user and assistant\n * @returns The assistant's response as a string\n */\nexport const askDocQuestion = async (\n messages: ChatCompletionRequestMessage[],\n aiConfig: AIConfig,\n options?: AskDocQuestionOptions\n): Promise<AskDocQuestionResult> => {\n // Format the user's question to keep only the relevant keywords\n const query = messages\n .filter((message) => message.role === 'user')\n .map((message) => `- ${message.content}`)\n .join('\\n');\n\n // Find relevant documents based on the user's question\n const relevantFilesReferences = await searchChunkReference(query);\n\n // Integrate the relevant documents into the initial system prompt\n const systemPrompt = initPrompt.content.replace(\n '{{relevantFilesReferences}}',\n relevantFilesReferences.length === 0\n ? 'Not relevant file found related to the question.'\n : relevantFilesReferences\n .map((doc, idx) =>\n [\n '-----',\n '---',\n `chunkId: ${idx}`,\n `docChunk: \"${doc.chunkNumber}/${doc.fileKey.length}\"`,\n `docName: \"${doc.docName}\"`,\n `docUrl: \"${doc.docUrl}\"`,\n `---`,\n doc.content,\n `-----`,\n ].join('\\n')\n )\n .join('\\n\\n') // Insert relevant docs into the prompt\n );\n\n // Format messages for AI SDK\n const aiMessages = [\n {\n role: 'system' as const,\n content: systemPrompt,\n },\n ...messages.slice(-8),\n ];\n\n if (!aiConfig) {\n throw new Error('Failed to initialize AI configuration');\n }\n\n // Use the AI SDK to stream the response\n let fullResponse = '';\n const stream = streamText({\n ...aiConfig,\n messages: aiMessages,\n });\n\n // Process the stream\n for await (const chunk of stream.textStream) {\n fullResponse += chunk;\n options?.onMessage?.(chunk);\n }\n\n // Extract unique related files\n const relatedFiles = [\n ...new Set(relevantFilesReferences.map((doc) => doc.fileKey)),\n ];\n\n // Return the assistant's response to the user\n return {\n response: fullResponse ?? 'Error: No result found',\n relatedFiles,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"askDocQuestion.mjs","names":[],"sources":["../../../../../src/utils/AI/askDocQuestion/askDocQuestion.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n type AIConfig,\n type ChatCompletionRequestMessage,\n streamText,\n} from '@intlayer/ai';\nimport { getMarkdownMetadata } from '@intlayer/core';\nimport { getBlogs, getDocs, getFrequentQuestions } from '@intlayer/docs';\nimport { OpenAI } from 'openai';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst readEmbeddingsForFile = (fileKey: string): Record<string, number[]> => {\n try {\n return JSON.parse(\n readFileSync(\n join(__dirname, `./embeddings/${fileKey.replace('.md', '.json')}`),\n 'utf-8'\n )\n ) as Record<string, number[]>;\n } catch {\n return {};\n }\n};\n\ntype VectorStoreEl = {\n fileKey: string;\n chunkNumber: number;\n content: string;\n embedding?: number[];\n docUrl: string;\n docName: string;\n};\n\n/**\n * Simple in-memory vector store to hold document embeddings and their content.\n * Each entry contains:\n * - fileKey: A unique key identifying the file\n * - chunkNumber: The number of the chunk within the document\n * - content: The chunk content\n * - embedding: The numerical embedding vector for the chunk\n */\nconst vectorStore: VectorStoreEl[] = [];\n\n/*\n * Ask question AI configuration\n */\nconst MAX_RELEVANT_CHUNKS_NB: number = 20; // Maximum number of relevant chunks to attach to chatGPT context\nconst MIN_RELEVANT_CHUNKS_SIMILARITY: number = 0.42; // Minimum similarity required for a chunk to be considered relevant\n\n/*\n * Embedding model configuration\n */\nconst EMBEDDING_MODEL: OpenAI.EmbeddingModel = 'text-embedding-3-large'; // Model to use for embedding generation\nconst OVERLAP_TOKENS: number = 200; // Number of tokens to overlap between chunks\nconst MAX_CHUNK_TOKENS: number = 800; // Maximum number of tokens per chunk\nconst CHAR_BY_TOKEN: number = 4.15; // Approximate pessimistically the number of characters per token // Can use `tiktoken` or other tokenizers to calculate it more precisely\nconst MAX_CHARS: number = MAX_CHUNK_TOKENS * CHAR_BY_TOKEN;\nconst OVERLAP_CHARS: number = OVERLAP_TOKENS * CHAR_BY_TOKEN;\n\nconst skipDocEmbeddingsIndex = process.env.SKIP_DOC_EMBEDDINGS_INDEX === 'true';\n\n/**\n * Splits a given text into chunks ensuring each chunk does not exceed MAX_CHARS.\n * @param text - The input text to split.\n * @returns - Array of text chunks.\n */\nconst chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let start = 0;\n\n while (start < text.length) {\n let end = Math.min(start + MAX_CHARS, text.length);\n\n // Ensure we don't cut words in the middle (find nearest space)\n if (end < text.length) {\n const lastSpace = text.lastIndexOf(' ', end);\n if (lastSpace > start) {\n end = lastSpace;\n }\n }\n\n chunks.push(text.substring(start, end));\n\n // Move start forward correctly\n const nextStart = end - OVERLAP_CHARS;\n if (nextStart <= start) {\n // Prevent infinite loop if overlap is too large\n start = end;\n } else {\n start = nextStart;\n }\n }\n\n return chunks;\n};\n\n/**\n * Generates an embedding for a given text using OpenAI's embedding API.\n *\n * @param text - The input text to generate an embedding for\n * @returns The embedding vector as a number array\n */\nconst generateEmbedding = async (text: string): Promise<number[]> => {\n // No try/catch here. If this fails, the controller should handle it.\n const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n const response = await openaiClient.embeddings.create({\n model: EMBEDDING_MODEL,\n input: text,\n });\n\n return response.data[0].embedding;\n};\n\n/**\n * Calculates the cosine similarity between two vectors.\n * Cosine similarity measures the cosine of the angle between two vectors in an inner product space.\n * Used to determine the similarity between chunks of text.\n *\n * @param vecA - The first vector\n * @param vecB - The second vector\n * @returns The cosine similarity score\n */\nconst cosineSimilarity = (vecA: number[], vecB: number[]): number => {\n // Calculate the dot product of the two vectors\n const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);\n\n // Calculate the magnitude (Euclidean norm) of each vector\n const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));\n const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));\n\n // Compute and return the cosine similarity\n return dotProduct / (magnitudeA * magnitudeB);\n};\n\n/**\n * Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.\n * Persists per-document embeddings under `embeddings/<fileKey>.json`.\n * Handles cases where files have been updated and chunk counts have changed.\n */\nexport const loadMarkdownFiles = async (): Promise<void> => {\n // Retrieve documentation and blog posts in English locale\n const frequentQuestions = await getFrequentQuestions();\n const docs = await getDocs();\n const blogs = await getBlogs();\n\n const files = { ...docs, ...blogs, ...frequentQuestions }; // Combine docs and blogs into a single object\n\n // Iterate over each file key (identifier) in the combined files\n for await (const fileKey of Object.keys(files)) {\n // Get the metadata of the file\n const fileMetadata = getMarkdownMetadata(\n files[fileKey as keyof typeof files] as string\n );\n\n // Split the document into chunks based on headings\n const fileChunks = chunkText(\n files[fileKey as keyof typeof files] as string\n );\n\n // Read existing embeddings for this file\n const existingEmbeddings = readEmbeddingsForFile(fileKey);\n\n // Check if the number of chunks has changed for this file\n const existingChunksForFile = Object.keys(existingEmbeddings);\n const currentChunkCount = fileChunks.length;\n const previousChunkCount = existingChunksForFile.length;\n\n let shouldRegenerateFileEmbeddings = false;\n\n // If chunk count differs, we need to regenerate embeddings for this file\n if (currentChunkCount !== previousChunkCount) {\n console.info(\n `File \"${fileKey}\" chunk count changed: ${previousChunkCount} -> ${currentChunkCount}. Regenerating embeddings.`\n );\n\n shouldRegenerateFileEmbeddings = !skipDocEmbeddingsIndex;\n }\n\n // Iterate over each chunk within the current file\n let resultForFile: Record<string, number[] | undefined> = {};\n for await (const chunkIndex of Object.keys(fileChunks)) {\n const chunkNumber = Number(chunkIndex) + 1; // Chunk number starts at 1\n const chunksNumber = fileChunks.length;\n\n const fileChunk = fileChunks[\n chunkIndex as keyof typeof fileChunks\n ] as string;\n\n const chunkKeyName = `chunk_${chunkNumber}`; // Unique key for the chunk within the file\n\n // Retrieve precomputed embedding if available and file hasn't changed\n const docEmbedding = !shouldRegenerateFileEmbeddings\n ? (existingEmbeddings[\n chunkKeyName as keyof typeof existingEmbeddings\n ] as number[] | undefined)\n : undefined;\n\n const embedding = docEmbedding; // Use existing embedding if available and valid\n\n // Update the file-scoped result object with the embedding\n resultForFile = { ...resultForFile, [chunkKeyName]: embedding };\n\n // Store the embedding and content in the in-memory vector store\n vectorStore.push({\n fileKey,\n chunkNumber,\n embedding,\n content: fileChunk,\n docUrl: fileMetadata.url,\n docName: fileMetadata.title,\n });\n\n console.info(`- Loaded: ${fileKey}/${chunkKeyName}/${chunksNumber}`);\n }\n }\n};\n\n// Automatically index Markdown files\nloadMarkdownFiles();\n\n/**\n * Searches the indexed documents for the most relevant chunks based on a query.\n * Utilizes cosine similarity to find the closest matching embeddings.\n *\n * @param query - The search query provided by the user\n * @returns An array of the top matching document chunks' content\n */\nexport const searchChunkReference = async (\n query: string,\n maxResults: number = MAX_RELEVANT_CHUNKS_NB,\n minSimilarity: number = MIN_RELEVANT_CHUNKS_SIMILARITY\n): Promise<VectorStoreEl[]> => {\n // Generate an embedding for the user's query\n const queryEmbedding = await generateEmbedding(query);\n\n // Calculate similarity scores between the query embedding and each document's embedding\n const selection = vectorStore\n .filter((chunk) => chunk.embedding)\n .map((chunk) => ({\n ...chunk,\n similarity: cosineSimilarity(queryEmbedding, chunk.embedding!), // Add similarity score to each doc\n }))\n .filter((chunk) => chunk.similarity > minSimilarity) // Filter out documents with low similarity scores\n .sort((a, b) => b.similarity - a.similarity) // Sort documents by highest similarity first\n .slice(0, maxResults); // Select the top 6 most similar documents\n\n const orderedDocKeys = new Set(selection.map((chunk) => chunk.fileKey));\n\n const orderedVectorStore = vectorStore.sort((a, _b) =>\n orderedDocKeys.has(a.fileKey) ? -1 : 1\n );\n\n const results = orderedVectorStore.filter((chunk) =>\n selection.some(\n (v) => v.fileKey === chunk.fileKey && v.chunkNumber === chunk.chunkNumber\n )\n );\n\n // Return the content of the top matching documents\n return results;\n};\n\nconst CHAT_GPT_PROMPT = readFileSync(join(__dirname, './PROMPT.md'), 'utf-8');\n\n// Initial prompt configuration for the chatbot\nexport const initPrompt: ChatCompletionRequestMessage = {\n role: 'system',\n content: CHAT_GPT_PROMPT,\n};\n\nexport type AskDocQuestionResult = {\n response: string;\n relatedFiles: string[];\n};\n\nexport type AskDocQuestionOptions = {\n onMessage?: (chunk: string) => void;\n};\n\n/**\n * Handles the \"Ask a question\" endpoint in an Express.js route.\n * Processes user messages, retrieves relevant documents, and interacts with AI models to generate responses.\n *\n * @param messages - An array of chat messages from the user and assistant\n * @returns The assistant's response as a string\n */\nexport const askDocQuestion = async (\n messages: ChatCompletionRequestMessage[],\n aiConfig: AIConfig,\n options?: AskDocQuestionOptions\n): Promise<AskDocQuestionResult> => {\n // Format the user's question to keep only the relevant keywords\n const query = messages\n .filter((message) => message.role === 'user')\n .map((message) => `- ${message.content}`)\n .join('\\n');\n\n // Find relevant documents based on the user's question\n const relevantFilesReferences = await searchChunkReference(query);\n\n // Integrate the relevant documents into the initial system prompt\n const systemPrompt = initPrompt.content.replace(\n '{{relevantFilesReferences}}',\n relevantFilesReferences.length === 0\n ? 'Not relevant file found related to the question.'\n : relevantFilesReferences\n .map((doc, idx) =>\n [\n '-----',\n '---',\n `chunkId: ${idx}`,\n `docChunk: \"${doc.chunkNumber}/${doc.fileKey.length}\"`,\n `docName: \"${doc.docName}\"`,\n `docUrl: \"${doc.docUrl}\"`,\n `---`,\n doc.content,\n `-----`,\n ].join('\\n')\n )\n .join('\\n\\n') // Insert relevant docs into the prompt\n );\n\n // Format messages for AI SDK\n const aiMessages = [\n {\n role: 'system' as const,\n content: systemPrompt,\n },\n ...messages.slice(-8),\n ];\n\n if (!aiConfig) {\n throw new Error('Failed to initialize AI configuration');\n }\n\n // Use the AI SDK to stream the response\n let fullResponse = '';\n const stream = streamText({\n ...aiConfig,\n messages: aiMessages,\n });\n\n // Process the stream\n for await (const chunk of stream.textStream) {\n fullResponse += chunk;\n options?.onMessage?.(chunk);\n }\n\n // Extract unique related files\n const relatedFiles = [\n ...new Set(relevantFilesReferences.map((doc) => doc.fileKey)),\n ];\n\n // Return the assistant's response to the user\n return {\n response: fullResponse ?? 'Error: No result found',\n relatedFiles,\n };\n};\n"],"mappings":"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"}