From 1694e8d9b788d148ec0678a5bab8c26ca8b2b131 Mon Sep 17 00:00:00 2001 From: LironEr Date: Fri, 20 May 2022 15:15:29 +0300 Subject: [PATCH 1/6] WIP: auth --- package.json | 2 +- packages/bundlemon-utils/src/consts.ts | 4 + service/package.json | 2 +- service/src/app.ts | 2 +- .../__snapshots__/commitRecords.spec.ts.snap | 3 + service/src/consts/commitRecords.ts | 4 + service/src/consts/schemas.ts | 166 +++++++++-- .../controllers/commitRecordsController.ts | 26 +- service/src/controllers/githubController.ts | 226 +++----------- .../src/controllers/legacyGithubController.ts | 282 ++++++++++++++++++ service/src/controllers/projectsController.ts | 17 +- .../src/controllers/subprojectsController.ts | 2 +- service/src/controllers/utils/auth.ts | 132 ++++---- service/src/framework/github.ts | 82 ++++- service/src/framework/mongo/client.ts | 45 +++ .../{mongo.ts => mongo/commitRecords.ts} | 112 +++---- service/src/framework/mongo/projects.ts | 63 ++++ .../api/__tests__/commitRecordsRoutes.spec.ts | 34 +-- .../api/__tests__/projectsRoutes.spec.ts | 89 +++++- .../api/__tests__/subprojectsRoutes.spec.ts | 12 +- service/src/routes/api/v1.ts | 12 +- service/src/types.ts | 18 ++ service/src/types/schemas/commitRecords.ts | 16 +- service/src/types/schemas/common.ts | 8 + service/src/types/schemas/githubOutput.ts | 22 +- service/src/types/schemas/projects.ts | 8 + service/src/utils/projectUtils.ts | 18 ++ service/src/utils/reportUtils.ts | 17 ++ service/tests/projectUtils.ts | 19 +- yarn.lock | 7 +- 30 files changed, 1046 insertions(+), 404 deletions(-) create mode 100644 service/src/controllers/legacyGithubController.ts create mode 100644 service/src/framework/mongo/client.ts rename service/src/framework/{mongo.ts => mongo/commitRecords.ts} (65%) create mode 100644 service/src/framework/mongo/projects.ts create mode 100644 service/src/types/schemas/projects.ts create mode 100644 service/src/utils/projectUtils.ts create mode 100644 service/src/utils/reportUtils.ts diff --git a/package.json b/package.json index 61bcc10..26b3584 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint-staged": "^11.1.2", "prettier": "^2.3.2", "ts-jest": "^27.0.4", - "typescript": "^4.3.5" + "typescript": "^4.6.4" }, "engines": { "yarn": "^1.10.0" diff --git a/packages/bundlemon-utils/src/consts.ts b/packages/bundlemon-utils/src/consts.ts index 161bde3..9392682 100644 --- a/packages/bundlemon-utils/src/consts.ts +++ b/packages/bundlemon-utils/src/consts.ts @@ -20,3 +20,7 @@ export enum FailReason { MaxSize = 'MaxSize', MaxPercentIncrease = 'MaxPercentIncrease', } + +export enum ProjectProvider { + GitHub = 'github', +} diff --git a/service/package.json b/service/package.json index 06d31bf..1eaa669 100644 --- a/service/package.json +++ b/service/package.json @@ -40,6 +40,6 @@ "ts-json-schema-generator": "^0.94.1", "ts-node": "^10.2.0", "vercel": "^21.3.3", - "typescript": "^4.3.5" + "typescript": "^4.6.4" } } diff --git a/service/src/app.ts b/service/src/app.ts index 6216d3a..db8333f 100644 --- a/service/src/app.ts +++ b/service/src/app.ts @@ -2,7 +2,7 @@ import fastify from 'fastify'; import routes from './routes'; import cors from '@fastify/cors'; import * as schemas from './consts/schemas'; -import { closeMongoClient } from './framework/mongo'; +import { closeMongoClient } from './framework/mongo/client'; function init() { const app = fastify({ diff --git a/service/src/consts/__tests__/__snapshots__/commitRecords.spec.ts.snap b/service/src/consts/__tests__/__snapshots__/commitRecords.spec.ts.snap index d8bc2e3..77478bc 100644 --- a/service/src/consts/__tests__/__snapshots__/commitRecords.spec.ts.snap +++ b/service/src/consts/__tests__/__snapshots__/commitRecords.spec.ts.snap @@ -12,5 +12,8 @@ Object { "Months": "months", "Weeks": "weeks", }, + "CreateCommitRecordAuthType": Object { + "GithubActions": "GITHUB_ACTIONS", + }, } `; diff --git a/service/src/consts/commitRecords.ts b/service/src/consts/commitRecords.ts index fff086e..508895d 100644 --- a/service/src/consts/commitRecords.ts +++ b/service/src/consts/commitRecords.ts @@ -9,3 +9,7 @@ export enum BaseRecordCompareTo { PreviousCommit = 'PREVIOUS_COMMIT', LatestCommit = 'LATEST_COMMIT', } + +export enum CreateCommitRecordAuthType { + GithubActions = 'GITHUB_ACTIONS', +} diff --git a/service/src/consts/schemas.ts b/service/src/consts/schemas.ts index 1d90153..1932dba 100644 --- a/service/src/consts/schemas.ts +++ b/service/src/consts/schemas.ts @@ -46,21 +46,13 @@ export const GithubActionsAuthHeaders = { type: 'string', const: 'GITHUB_ACTION', }, - 'github-owner': { - type: 'string', - minLength: 1, - }, - 'github-repo': { - type: 'string', - minLength: 1, - }, 'github-run-id': { type: 'string', minLength: 1, pattern: '^\\d+$', }, }, - required: ['bundlemon-auth-type', 'github-owner', 'github-repo', 'github-run-id'], + required: ['bundlemon-auth-type', 'github-run-id'], additionalProperties: false, }; @@ -88,21 +80,13 @@ export const AuthHeaders = { type: 'string', const: 'GITHUB_ACTION', }, - 'github-owner': { - type: 'string', - minLength: 1, - }, - 'github-repo': { - type: 'string', - minLength: 1, - }, 'github-run-id': { type: 'string', minLength: 1, pattern: '^\\d+$', }, }, - required: ['bundlemon-auth-type', 'github-owner', 'github-repo', 'github-run-id'], + required: ['bundlemon-auth-type', 'github-run-id'], }, ], }; @@ -761,8 +745,8 @@ export const PostGithubPRCommentRequestSchema = { additionalProperties: false, }; -export const GithubOutputRequestSchema = { - $id: '#/definitions/GithubOutputRequestSchema', +export const LegacyGithubOutputRequestSchema = { + $id: '#/definitions/LegacyGithubOutputRequestSchema', type: 'object', properties: { body: { @@ -829,6 +813,148 @@ export const GithubOutputRequestSchema = { additionalProperties: false, }; +export const GithubOutputRequestSchema = { + $id: '#/definitions/GithubOutputRequestSchema', + type: 'object', + properties: { + body: { + type: 'object', + properties: { + report: { + $ref: '#/definitions/Report', + }, + git: { + anyOf: [ + { + type: 'object', + additionalProperties: false, + properties: { + token: { + type: 'string', + }, + owner: { + type: 'string', + }, + repo: { + type: 'string', + }, + commitSha: { + type: 'string', + }, + prNumber: { + type: 'string', + }, + }, + required: ['commitSha', 'owner', 'repo', 'token'], + }, + { + type: 'object', + additionalProperties: false, + properties: { + actionId: { + type: 'string', + }, + owner: { + type: 'string', + }, + repo: { + type: 'string', + }, + commitSha: { + type: 'string', + }, + prNumber: { + type: 'string', + }, + }, + required: ['actionId', 'commitSha', 'owner', 'repo'], + }, + ], + }, + output: { + type: 'object', + properties: { + checkRun: { + type: 'boolean', + }, + commitStatus: { + type: 'boolean', + }, + prComment: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + }, + required: ['report', 'git', 'output'], + additionalProperties: false, + }, + query: {}, + params: { + type: 'object', + properties: { + projectId: { + type: 'string', + pattern: '^[0-9a-fA-F]{24}$', + }, + }, + required: ['projectId'], + additionalProperties: false, + }, + headers: { + $ref: '#/definitions/AuthHeaders', + }, + }, + required: ['body', 'params', 'headers'], + additionalProperties: false, +}; + +export const GetOrCreateProjectIdRequestSchema = { + $id: '#/definitions/GetOrCreateProjectIdRequestSchema', + type: 'object', + properties: { + body: { + $ref: '#/definitions/GitDetails', + }, + query: {}, + params: {}, + headers: {}, + }, + required: ['body'], + additionalProperties: false, +}; + +export const GitDetails = { + $id: '#/definitions/GitDetails', + type: 'object', + properties: { + provider: { + $ref: '#/definitions/ProjectProvider', + }, + owner: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '^[a-zA-Z0-9_.-]*$', + }, + repo: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '^[a-zA-Z0-9_.-]*$', + }, + }, + required: ['provider', 'owner', 'repo'], + additionalProperties: false, +}; + +export const ProjectProvider = { + $id: '#/definitions/ProjectProvider', + type: 'string', + const: 'github', +}; + export const GetSubprojectsRequestSchema = { $id: '#/definitions/GetSubprojectsRequestSchema', type: 'object', diff --git a/service/src/controllers/commitRecordsController.ts b/service/src/controllers/commitRecordsController.ts index b26cbaa..3ef7e22 100644 --- a/service/src/controllers/commitRecordsController.ts +++ b/service/src/controllers/commitRecordsController.ts @@ -1,5 +1,5 @@ -import { createCommitRecord, getCommitRecords, getCommitRecord } from '../framework/mongo'; -import { checkAuthHeaders } from './utils/auth'; +import { createCommitRecord, getCommitRecords, getCommitRecordWithBase } from '../framework/mongo/commitRecords'; +import { checkAuth } from './utils/auth'; import { generateLinkToReport } from '../utils/linkUtils'; import { BaseRecordCompareTo } from '../consts/commitRecords'; @@ -9,7 +9,7 @@ import type { GetCommitRecordRequestSchema, GetCommitRecordsRequestSchema, } from '../types/schemas'; -import type { BaseCommitRecordResponse, CommitRecord, CreateCommitRecordResponse } from 'bundlemon-utils'; +import type { CommitRecord, CreateCommitRecordResponse } from 'bundlemon-utils'; export const getCommitRecordsController: FastifyValidatedRoute = async (req, res) => { const records = await getCommitRecords(req.params.projectId, req.query); @@ -24,9 +24,10 @@ export const createCommitRecordController: FastifyValidatedRoute = async (req, res) => { +// bundlemon > v2.0.0 +export const githubOutputController: FastifyValidatedRoute = async (req, res) => { try { const { - params: { projectId }, - headers, - body: { - git: { owner, repo, commitSha }, - report, - }, + params: { projectId, commitRecordId }, + body: { git, output }, } = req; - const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + const project = await getProject(projectId); - if (!installationOctokit) { + if (!project) { + res.log.warn({ projectId }, 'project id not found'); + res.status(404).send({ error: 'project not found' }); return; } - const summary = generateReportMarkdownWithLinks(report); - - req.log.info(`summary length: ${summary.length}`); - - const checkRes = await createCheck({ - owner, - repo, - commitSha, - installationOctokit, - detailsUrl: report.metadata.linkToReport || undefined, - title: getReportConclusionText(report), - summary, - conclusion: report.status === Status.Pass ? 'success' : 'failure', - log: req.log, - }); - - res.send(checkRes); - } catch (err) { - req.log.error(err); - - res.status(500).send({ - message: 'failed to create check', - error: (err as Error).message, - }); - } -}; - -// bundlemon <= v0.4.0 -export const createGithubCommitStatusController: FastifyValidatedRoute = async ( - req, - res -) => { - try { - const { - params: { projectId }, - headers, - body: { - git: { owner, repo, commitSha }, - report, - }, - } = req; - - const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + const { record, baseRecord } = + (await getCommitRecordWithBase({ projectId, commitRecordId }, BaseRecordCompareTo.PreviousCommit)) ?? {}; - if (!installationOctokit) { + if (!record) { + req.log.warn({ commitRecordId, projectId }, 'commit record not found for project'); + res.status(404).send({ error: 'commit record not found for project' }); return; } - const checkRes = await createCommitStatus({ - owner, - repo, - commitSha, - installationOctokit, - state: report.status === Status.Pass ? 'success' : 'error', - description: getReportConclusionText(report), - targetUrl: report.metadata.linkToReport || undefined, - log: req.log, - }); + const { owner, repo, commitSha, prNumber } = git; - res.send(checkRes); - } catch (err) { - req.log.error(err); + let gitClient: Octokit | undefined; - res.status(500).send({ - message: 'failed to create commit status', - error: (err as Error).message, - }); - } -}; + if ('token' in git) { + gitClient = createOctokitClientByToken(git.token); + } else if ('runId' in git) { + if (!isGitHubProject(project, res.log)) { + res.status(403).send({ error: 'forbidden' }); + return; + } -// bundlemon <= v0.4.0 -export const postGithubPRCommentController: FastifyValidatedRoute = async ( - req, - res -) => { - try { - const { - params: { projectId }, - headers, - body: { - git: { owner, repo, prNumber }, - report, - }, - } = req; + if (project.owner !== owner || project.owner !== owner || project.owner !== owner) { + res.log.warn('mismatch between project git details to payload git details'); + res.status(403).send({ error: 'forbidden: mismatch between project git details to payload git details' }); + return; + } + const result = await createOctokitClientByAction(git, res.log); - const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + if (!result.authenticated) { + res.status(403).send({ error: result.error }); + return; + } - if (!installationOctokit) { - return; + gitClient = result.installationOctokit; } - const body = `${genCommentIdentifier()}\n## BundleMon\n${generateReportMarkdownWithLinks(report)}`; - - const checkRes = await createOrUpdatePRComment({ - owner, - repo, - prNumber, - installationOctokit, - body, - log: req.log, - }); - - res.send(checkRes); - } catch (err) { - req.log.error(err); - - res.status(500).send({ - message: 'failed to post PR comment', - error: (err as Error).message, - }); - } -}; - -// bundlemon > v0.4 -export const githubOutputController: FastifyValidatedRoute = async (req, res) => { - try { - const { - params: { projectId }, - headers, - body: { - git: { owner, repo, commitSha, prNumber }, - report, - output, - }, - } = req; - - const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); - - if (!installationOctokit) { + if (!gitClient) { + res.log.warn({ owner, repo, commitSha }, 'no git client'); + res.status(403).send({ error: 'forbidden' }); return; } + const report = generateReport({ record, baseRecord }); const { subProject } = report.metadata; const tasks: Partial>> = {}; @@ -222,7 +94,7 @@ export const githubOutputController: FastifyValidatedRoute = async (req, res) => { + try { + const { + params: { projectId }, + headers, + body: { + git: { owner, repo, commitSha }, + report, + }, + } = req; + + const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + + if (!installationOctokit) { + return; + } + + const summary = generateReportMarkdownWithLinks(report); + + req.log.info(`summary length: ${summary.length}`); + + const checkRes = await createCheck({ + owner, + repo, + commitSha, + installationOctokit, + detailsUrl: report.metadata.linkToReport || undefined, + title: getReportConclusionText(report), + summary, + conclusion: report.status === Status.Pass ? 'success' : 'failure', + log: req.log, + }); + + res.send(checkRes); + } catch (err) { + req.log.error(err); + + res.status(500).send({ + message: 'failed to create check', + error: (err as Error).message, + }); + } +}; + +// bundlemon <= v0.4.0 +export const createGithubCommitStatusController: FastifyValidatedRoute = async ( + req, + res +) => { + try { + const { + params: { projectId }, + headers, + body: { + git: { owner, repo, commitSha }, + report, + }, + } = req; + + const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + + if (!installationOctokit) { + return; + } + + const checkRes = await createCommitStatus({ + owner, + repo, + commitSha, + installationOctokit, + state: report.status === Status.Pass ? 'success' : 'error', + description: getReportConclusionText(report), + targetUrl: report.metadata.linkToReport || undefined, + log: req.log, + }); + + res.send(checkRes); + } catch (err) { + req.log.error(err); + + res.status(500).send({ + message: 'failed to create commit status', + error: (err as Error).message, + }); + } +}; + +// bundlemon <= v0.4.0 +export const postGithubPRCommentController: FastifyValidatedRoute = async ( + req, + res +) => { + try { + const { + params: { projectId }, + headers, + body: { + git: { owner, repo, prNumber }, + report, + }, + } = req; + + const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + + if (!installationOctokit) { + return; + } + + const body = `${genCommentIdentifier()}\n## BundleMon\n${generateReportMarkdownWithLinks(report)}`; + + const checkRes = await createOrUpdatePRComment({ + owner, + repo, + prNumber, + installationOctokit, + body, + log: req.log, + }); + + res.send(checkRes); + } catch (err) { + req.log.error(err); + + res.status(500).send({ + message: 'failed to post PR comment', + error: (err as Error).message, + }); + } +}; + +// bundlemon > v0.4 +export const legacyGithubOutputController: FastifyValidatedRoute = async ( + req, + res +) => { + try { + const { + params: { projectId }, + headers, + body: { + git: { owner, repo, commitSha, prNumber }, + report, + output, + }, + } = req; + + const installationOctokit = await getInstallationOctokit({ projectId, headers, owner, repo }, res); + + if (!installationOctokit) { + return; + } + + const { subProject } = report.metadata; + const tasks: Partial>> = {}; + + if (output.checkRun) { + const summary = generateReportMarkdownWithLinks(report); + + tasks.checkRun = createCheck({ + subProject, + owner, + repo, + commitSha, + installationOctokit, + detailsUrl: report.metadata.linkToReport || undefined, + title: getReportConclusionText(report), + summary, + conclusion: report.status === Status.Pass ? 'success' : 'failure', + log: req.log, + }); + } + + if (output.commitStatus) { + tasks.commitStatus = createCommitStatus({ + subProject, + owner, + repo, + commitSha, + installationOctokit, + state: report.status === Status.Pass ? 'success' : 'error', + description: getReportConclusionText(report), + targetUrl: report.metadata.linkToReport || undefined, + log: req.log, + }); + } + + if (output.prComment) { + const title = subProject ? `BundleMon (${subProject})` : 'BundleMon'; + const body = `${genCommentIdentifier(subProject)}\n## ${title}\n${generateReportMarkdownWithLinks(report)}`; + + tasks.prComment = createOrUpdatePRComment({ + subProject, + owner, + repo, + prNumber, + installationOctokit, + body, + log: req.log, + }); + } + + const response: GithubOutputResponse = await promiseAllObject(tasks); + + res.send(response); + } catch (err) { + req.log.error(err); + + res.status(500).send({ + message: 'failed to post GitHub output', + error: (err as Error).message, + }); + } +}; diff --git a/service/src/controllers/projectsController.ts b/service/src/controllers/projectsController.ts index 6f94b4a..9c87d6d 100644 --- a/service/src/controllers/projectsController.ts +++ b/service/src/controllers/projectsController.ts @@ -1,9 +1,11 @@ import { randomBytes } from 'crypto'; -import { createProject } from '../framework/mongo'; +import { createProject, getOrCreateProjectId } from '../framework/mongo/projects'; import { createHash } from '../utils/hashUtils'; import type { CreateProjectResponse } from 'bundlemon-utils'; import type { RouteHandlerMethod } from 'fastify'; +import type { FastifyValidatedRoute } from '../types/schemas'; +import type { GetOrCreateProjectIdRequestSchema } from '../types/schemas/projects'; export const createProjectController: RouteHandlerMethod = async (_req, res) => { const apiKey = randomBytes(32).toString('hex'); @@ -16,3 +18,16 @@ export const createProjectController: RouteHandlerMethod = async (_req, res) => res.send(response); }; + +export const getOrCreateProjectIdController: FastifyValidatedRoute = async ( + req, + res +) => { + const { provider, owner, repo } = req.body; + + // TODO: use checkAuth? + + const id = await getOrCreateProjectId({ provider, owner: owner.toLowerCase(), repo: repo.toLowerCase() }); + + res.send({ id }); +}; diff --git a/service/src/controllers/subprojectsController.ts b/service/src/controllers/subprojectsController.ts index e1f9d7d..5d8fb65 100644 --- a/service/src/controllers/subprojectsController.ts +++ b/service/src/controllers/subprojectsController.ts @@ -1,4 +1,4 @@ -import { getSubprojects } from '../framework/mongo'; +import { getSubprojects } from '../framework/mongo/commitRecords'; import type { FastifyValidatedRoute } from '../types/schemas'; import type { GetSubprojectsRequestSchema } from '../types/schemas/subprojects'; diff --git a/service/src/controllers/utils/auth.ts b/service/src/controllers/utils/auth.ts index ff3d530..1cfdeed 100644 --- a/service/src/controllers/utils/auth.ts +++ b/service/src/controllers/utils/auth.ts @@ -1,12 +1,14 @@ import { Octokit } from '@octokit/rest'; -import { getProjectApiKeyHash } from '../../framework/mongo'; +import { getProject, Project } from '../../framework/mongo/projects'; import { verifyHash } from '../../utils/hashUtils'; -import { getInstallationId, createInstallationOctokit } from '../../framework/github'; +import { createOctokitClientByAction } from '../../framework/github'; +import { CreateCommitRecordAuthType } from '../../consts/commitRecords'; +import { isGitHubProject } from '../../utils/projectUtils'; import type { FastifyLoggerInstance } from 'fastify'; -import type { AuthHeaders, ProjectAuthHeaders, GithubActionsAuthHeaders } from '../../types/schemas'; +import type { AuthHeaders, CreateCommitRecordRequestQuery, GithubActionsAuthHeaders } from '../../types/schemas'; -type CheckAuthHeadersResponse = +type CheckAuthResponse = | { authenticated: false; error: string; @@ -14,79 +16,85 @@ type CheckAuthHeadersResponse = } | { authenticated: true; installationOctokit?: Octokit }; -export async function checkAuthHeaders( +export async function checkAuth( projectId: string, headers: AuthHeaders, + query: CreateCommitRecordRequestQuery | Record, + commitSha: string | undefined, log: FastifyLoggerInstance -): Promise { - const hash = await getProjectApiKeyHash(projectId); +): Promise { + const project = await getProject(projectId); - if (!hash) { + if (!project) { log.warn({ projectId }, 'project id not found'); return { authenticated: false, error: 'forbidden' }; } - const { 'bundlemon-auth-type': authType } = headers; + if ('x-api-key' in headers) { + return handleProjectAuth(project, headers['x-api-key'], log); + } - if (!authType || authType === 'API_KEY') { - const { 'x-api-key': apiKey } = headers as ProjectAuthHeaders; + // deprecated + if (headers['bundlemon-auth-type'] === 'GITHUB_ACTION') { + return handleLegacyGithubActionAuth(project, headers, log); + } - const isAuthenticated = await verifyHash(apiKey, hash); + if ('authType' in query && query.authType === CreateCommitRecordAuthType.GithubActions) { + return handleGithubActionAuth(project, { runId: query.runId, commitSha }, log); + } - if (isAuthenticated) { - return { authenticated: true }; - } + log.warn({ projectId: project.id }, 'unknown auth'); - log.warn({ projectId }, 'wrong API key'); - return { authenticated: isAuthenticated, error: 'forbidden' }; - } else if (authType === 'GITHUB_ACTION') { - const { 'github-owner': owner, 'github-repo': repo, 'github-run-id': runId } = headers as GithubActionsAuthHeaders; + return { authenticated: false, error: 'forbidden' }; +} - const installationId = await getInstallationId(owner, repo); +async function handleProjectAuth( + project: Project, + apiKey: string, + log: FastifyLoggerInstance +): Promise { + if (!('apiKey' in project)) { + log.warn({ projectId: project.id }, 'API key sent, but project dont have API key'); + return { authenticated: false, error: 'forbidden' }; + } - if (!installationId) { - log.info({ projectId, owner, repo }, 'missing installation id'); - return { authenticated: false, error: `BundleMon GitHub app is not installed on this repo (${owner}/${repo})` }; - } + const isAuthenticated = await verifyHash(apiKey, project.apiKey.hash); - const octokit = createInstallationOctokit(installationId); - - try { - const res = await octokit.actions.getWorkflowRun({ owner, repo, run_id: Number(runId) }); - - // check job status - if (!['in_progress', 'queued'].includes(res.data.status ?? '')) { - log.warn( - { projectId, runId, status: res.data.status, createdAt: res.data.created_at, updatedAt: res.data.updated_at }, - 'GitHub action should be in_progress/queued status' - ); - return { - authenticated: false, - error: `GitHub action status should be "in_progress" or "queued"`, - extraData: { - actionId: runId, - status: res.data.status, - workflowId: res.data.workflow_id, - createdAt: res.data.created_at, - updatedAt: res.data.updated_at, - }, - }; - } - - return { authenticated: true, installationOctokit: octokit }; - } catch (err) { - let errorMsg = 'forbidden'; - - if ((err as any).status === 404) { - errorMsg = `GitHub action ${runId} not found for ${owner}/${repo}`; - log.warn({ projectId }, 'workflow not found'); - } else { - log.warn({ err, projectId }, 'error during getWorkflowRun'); - } - - return { authenticated: false, error: errorMsg }; - } + if (isAuthenticated) { + return { authenticated: true }; } - return { authenticated: false, error: 'forbidden' }; + log.warn({ projectId: project.id }, 'wrong API key'); + return { authenticated: isAuthenticated, error: 'forbidden' }; +} + +async function handleLegacyGithubActionAuth( + project: Project, + headers: GithubActionsAuthHeaders, + log: FastifyLoggerInstance +): Promise { + const { 'github-owner': owner, 'github-repo': repo, 'github-run-id': runId } = headers as GithubActionsAuthHeaders; + + if ('provider' in project) { + log.warn({ projectId: project.id }, 'legacy github auth works only with old projects'); + return { authenticated: false, error: 'forbidden' }; + } + + return createOctokitClientByAction({ owner, repo, runId }, log); +} + +async function handleGithubActionAuth( + project: Project, + { runId, commitSha }: { runId: string; commitSha?: string }, + log: FastifyLoggerInstance +): Promise { + if (!isGitHubProject(project, log)) { + return { authenticated: false, error: 'forbidden' }; + } + + const { owner, repo } = project; + + const result = await createOctokitClientByAction({ owner, repo, runId, commitSha }, log); + + return result; } diff --git a/service/src/framework/github.ts b/service/src/framework/github.ts index 915fb56..2ad699e 100644 --- a/service/src/framework/github.ts +++ b/service/src/framework/github.ts @@ -2,9 +2,9 @@ import { githubAppId, githubAppPrivateKey } from './env'; import { Octokit } from '@octokit/rest'; import { createAppAuth } from '@octokit/auth-app'; +import type { OutputResponse } from 'bundlemon-utils'; import type { RequestError } from '@octokit/types'; import type { FastifyLoggerInstance } from 'fastify'; -import type { OutputResponse } from 'bundlemon-utils'; let _app: Octokit | undefined; @@ -47,15 +47,71 @@ export const getInstallationId = async (owner: string, repo: string): Promise => { - const res = await getApp().auth({ type: 'installation', installationId }); +type CreateOctokitClientByActionResponse = + | { + authenticated: false; + error: string; + extraData?: Record; + } + | { authenticated: true; installationOctokit: Octokit }; + +export async function createOctokitClientByAction( + { owner, repo, runId }: { owner: string; repo: string; commitSha?: string; runId: string }, + log: FastifyLoggerInstance +): Promise { + const installationId = await getInstallationId(owner, repo); - // @ts-ignore - return res.token; -}; + if (!installationId) { + log.info({ owner, repo }, 'missing installation id'); + return { authenticated: false, error: `BundleMon GitHub app is not installed on this repo (${owner}/${repo})` }; + } + + const octokit = createOctokitClientByInstallationId(installationId); + + try { + const res = await octokit.actions.getWorkflowRun({ owner, repo, run_id: Number(runId) }); + + // check job status + if (!['in_progress', 'queued'].includes(res.data.status ?? '')) { + log.warn( + { runId, status: res.data.status, createdAt: res.data.created_at, updatedAt: res.data.updated_at }, + 'GitHub action should be in_progress/queued status' + ); + return { + authenticated: false, + error: `GitHub action status should be "in_progress" or "queued"`, + extraData: { + actionId: runId, + status: res.data.status, + workflowId: res.data.workflow_id, + createdAt: res.data.created_at, + updatedAt: res.data.updated_at, + }, + }; + } -export function createInstallationOctokit(installationId: number) { - const installationOctokit = new Octokit({ + // TODO: validate action commit + + return { + authenticated: true, + installationOctokit: octokit, + }; + } catch (err) { + let errorMsg = 'forbidden'; + + if ((err as any).status === 404) { + errorMsg = `GitHub action ${runId} not found for ${owner}/${repo}`; + log.warn({ owner, repo, runId }, 'workflow not found'); + } else { + log.warn({ err, owner, repo, runId }, 'error during getWorkflowRun'); + } + + return { authenticated: false, error: errorMsg }; + } +} + +export function createOctokitClientByInstallationId(installationId: number) { + const client = new Octokit({ authStrategy: createAppAuth, auth: { ...getAppAuth(), @@ -63,7 +119,15 @@ export function createInstallationOctokit(installationId: number) { }, }); - return installationOctokit; + return client; +} + +export function createOctokitClientByToken(token: string) { + const client = new Octokit({ + auth: token, + }); + + return client; } interface CreateCheckParams { diff --git a/service/src/framework/mongo/client.ts b/service/src/framework/mongo/client.ts new file mode 100644 index 0000000..4d62e59 --- /dev/null +++ b/service/src/framework/mongo/client.ts @@ -0,0 +1,45 @@ +import { MongoClient, ReadPreference, Db, MongoClientOptions } from 'mongodb'; +import { mongoUrl, mongoDbName, nodeEnv, mongoDbUser, mongoDbPassword } from '../env'; + +let client: MongoClient | undefined; +let db: Db | undefined; + +const getClient = async () => { + if (!client) { + try { + const auth: MongoClientOptions['auth'] = + nodeEnv === 'production' ? { username: mongoDbUser, password: mongoDbPassword } : undefined; + + client = await MongoClient.connect(`${mongoUrl}/${mongoDbName}?retryWrites=true&w=majority`, { + auth, + readPreference: ReadPreference.PRIMARY, + }); + } catch (err) { + throw new Error('Could not connect to mongo\n ' + err); + } + } + + return client; +}; + +export async function closeMongoClient() { + if (client) { + return client.close(); + } +} + +export const getDB = async () => { + if (!db) { + try { + const client = await getClient(); + + db = client.db(mongoDbName); + } catch (err) { + throw new Error('Could not connect to mongo\n ' + err); + } + } + + return db; +}; + +export const getCollection = async (collectionName: string) => (await getDB()).collection(collectionName); diff --git a/service/src/framework/mongo.ts b/service/src/framework/mongo/commitRecords.ts similarity index 65% rename from service/src/framework/mongo.ts rename to service/src/framework/mongo/commitRecords.ts index c0cc11d..6617be7 100644 --- a/service/src/framework/mongo.ts +++ b/service/src/framework/mongo/commitRecords.ts @@ -1,84 +1,17 @@ -import { MongoClient, ReadPreference, Db, ObjectId, WithId, MongoClientOptions, ReturnDocument, Filter } from 'mongodb'; -import { mongoUrl, mongoDbName, nodeEnv, mongoDbUser, mongoDbPassword } from './env'; -import { CommitRecordsQueryResolution } from '../consts/commitRecords'; +import { ObjectId, WithId, ReturnDocument, Filter } from 'mongodb'; +import { BaseRecordCompareTo, CommitRecordsQueryResolution } from '../../consts/commitRecords'; +import { getCollection } from './client'; import type { CommitRecordPayload, CommitRecord } from 'bundlemon-utils'; -import type { GetCommitRecordsQuery } from '../types/schemas'; -import type { ProjectApiKey } from '../types'; +import type { GetCommitRecordsQuery } from '../../types/schemas'; interface CommitRecordDB extends CommitRecordPayload { projectId: string; creationDate: Date; } -interface ProjectDB { - apiKey: ProjectApiKey; - creationDate: Date; -} - -let client: MongoClient | undefined; -let db: Db | undefined; - -const getClient = async () => { - if (!client) { - try { - const auth: MongoClientOptions['auth'] = - nodeEnv === 'production' ? { username: mongoDbUser, password: mongoDbPassword } : undefined; - - client = await MongoClient.connect(`${mongoUrl}/${mongoDbName}?retryWrites=true&w=majority`, { - auth, - readPreference: ReadPreference.PRIMARY, - }); - } catch (err) { - throw new Error('Could not connect to mongo\n ' + err); - } - } - - return client; -}; - -export async function closeMongoClient() { - if (client) { - return client.close(); - } -} - -export const getDB = async () => { - if (!db) { - try { - const client = await getClient(); - - db = client.db(mongoDbName); - } catch (err) { - throw new Error('Could not connect to mongo\n ' + err); - } - } - - return db; -}; - -const getCollection = async (collectionName: string) => (await getDB()).collection(collectionName); - -export const getProjectsCollection = () => getCollection('projects'); export const getCommitRecordsCollection = () => getCollection('commitRecords'); -export const createProject = async (apiKey: ProjectApiKey): Promise => { - const projectsCollection = await getProjectsCollection(); - const id = (await projectsCollection.insertOne({ apiKey, creationDate: new Date() })).insertedId; - - return id.toHexString(); -}; - -export const getProjectApiKeyHash = async (projectId: string): Promise => { - const projectsCollection = await getProjectsCollection(); - const data = await projectsCollection.findOne<{ apiKey: { hash: string } }>( - { _id: new ObjectId(projectId) }, - { projection: { 'apiKey.hash': 1, _id: 0 } } - ); - - return data?.apiKey?.hash; -}; - const commitRecordDBToResponse = (record: WithId): CommitRecord => { const { _id, creationDate, ...restRecord } = record; @@ -107,13 +40,15 @@ export const createCommitRecord = async (projectId: string, record: CommitRecord return commitRecordDBToResponse(newRecord); }; +interface GetCommitRecordParams { + projectId: string; + commitRecordId: string; +} + export const getCommitRecord = async ({ projectId, commitRecordId, -}: { - projectId: string; - commitRecordId: string; -}): Promise => { +}: GetCommitRecordParams): Promise => { const commitRecordsCollection = await getCommitRecordsCollection(); const record = await commitRecordsCollection.findOne>({ _id: new ObjectId(commitRecordId), @@ -233,3 +168,30 @@ export async function getSubprojects(projectId: string) { return subProjects.filter((s) => !!s); } + +export interface CommitRecordWithBase { + record: CommitRecord; + baseRecord?: CommitRecord; +} + +export async function getCommitRecordWithBase( + { projectId, commitRecordId }: GetCommitRecordParams, + compareTo = BaseRecordCompareTo.PreviousCommit +): Promise { + const record = await getCommitRecord({ projectId, commitRecordId }); + + if (!record) { + return undefined; + } + + const baseRecord = ( + await getCommitRecords(projectId, { + branch: record.baseBranch ?? record.branch, + subProject: record.subProject, + latest: true, + olderThan: compareTo === BaseRecordCompareTo.PreviousCommit ? new Date(record.creationDate) : undefined, + }) + )?.[0]; + + return { record, baseRecord }; +} diff --git a/service/src/framework/mongo/projects.ts b/service/src/framework/mongo/projects.ts new file mode 100644 index 0000000..b189c55 --- /dev/null +++ b/service/src/framework/mongo/projects.ts @@ -0,0 +1,63 @@ +import { ObjectId } from 'mongodb'; +import { getCollection } from './client'; + +import type { ProjectApiKey, GitDetails } from '../../types'; + +type WithSimpleId = T & { id: string }; + +type BaseProjectDB = { + creationDate: Date; + lastAccessed: Date; +}; + +type ApiKeyProjectDB = BaseProjectDB & { + apiKey: ProjectApiKey; +}; +export type ApiKeyProject = WithSimpleId; + +type GitProjectDB = BaseProjectDB & GitDetails; +export type GitProject = WithSimpleId; + +export type ProjectDB = ApiKeyProjectDB | GitProjectDB; +export type Project = ApiKeyProject | GitProject; + +export const getProjectsCollection = () => getCollection('projects'); + +export const createProject = async (apiKey: ProjectApiKey): Promise => { + const projectsCollection = await getProjectsCollection(); + const id = (await projectsCollection.insertOne({ apiKey, creationDate: new Date(), lastAccessed: new Date() })) + .insertedId; + + return id.toHexString(); +}; + +// TODO: update lastAccess? +export const getProject = async (projectId: string): Promise => { + const projectsCollection = await getProjectsCollection(); + const data = await projectsCollection.findOne({ _id: new ObjectId(projectId) }); + + if (data) { + const { _id, ...rest } = data; + return { id: _id.toHexString(), ...rest }; + } + + return undefined; +}; + +export const getOrCreateProjectId = async (details: GitDetails): Promise => { + const projectsCollection = await getProjectsCollection(); + const result = await projectsCollection.findOneAndUpdate( + details, + { + $setOnInsert: { creationDate: new Date() }, + $set: { lastAccessed: new Date() }, + }, + { upsert: true, returnDocument: 'after' } + ); + + if (result.ok === 1 && result.value) { + return result.value._id.toHexString(); + } + + throw new Error('failed to get or update project'); +}; diff --git a/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts b/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts index 94b8c32..8f8fba3 100644 --- a/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts +++ b/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts @@ -8,16 +8,16 @@ import { BaseCommitRecordResponse, } from 'bundlemon-utils'; import { app } from '@tests/app'; -import { createTestProject } from '@tests/projectUtils'; +import { createTestProjectWithApiKey } from '@tests/projectUtils'; import { generateRandomString } from '@tests/utils'; -import { createCommitRecord, getCommitRecordsCollection } from '../../../framework/mongo'; +import { createCommitRecord, getCommitRecordsCollection } from '../../../framework/mongo/commitRecords'; import { generateLinkToReport } from '../../../utils/linkUtils'; import { BaseRecordCompareTo } from '../../..//consts/commitRecords'; describe('commit records routes', () => { describe('get commit records', () => { test('without branch', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const response = await app.inject({ method: 'GET', @@ -28,7 +28,7 @@ describe('commit records routes', () => { }); test('no records', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const branch = 'main'; @@ -54,7 +54,7 @@ describe('commit records routes', () => { test.each([{ name: 'without sub project' }, { name: 'with sub project', subProject: 'website2' }])( 'with records, $name', async ({ subProject }) => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const branch = 'main'; @@ -123,7 +123,7 @@ describe('commit records routes', () => { describe('create commit record', () => { test('not authenticated', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const payload: CommitRecordPayload = { branch: 'test', @@ -147,7 +147,7 @@ describe('commit records routes', () => { describe('without base branch', () => { test('no records in current branch', async () => { - const { projectId, apiKey } = await createTestProject(); + const { projectId, apiKey } = await createTestProjectWithApiKey(); const payload: CommitRecordPayload = { branch: 'test', @@ -188,7 +188,7 @@ describe('commit records routes', () => { }); test('with records in current branch', async () => { - const { projectId, apiKey } = await createTestProject(); + const { projectId, apiKey } = await createTestProjectWithApiKey(); await createCommitRecord(projectId, { branch: 'main', @@ -257,7 +257,7 @@ describe('commit records routes', () => { { name: 'base branch not found, with sub project', baseBranch: 'new', subProject: 'website2' }, { name: 'base branch has commit records, with sub project', baseBranch: 'main', subProject: 'website2' }, ])('$name', async ({ baseBranch, subProject }) => { - const { projectId, apiKey } = await createTestProject(); + const { projectId, apiKey } = await createTestProjectWithApiKey(); await createCommitRecord(projectId, { branch: 'main', @@ -332,7 +332,7 @@ describe('commit records routes', () => { }); test('commit sha already exists - overwrite', async () => { - const { projectId, apiKey } = await createTestProject(); + const { projectId, apiKey } = await createTestProjectWithApiKey(); const commitSha = generateRandomString(8); const originalRecord = await createCommitRecord(projectId, { @@ -384,7 +384,7 @@ describe('commit records routes', () => { }); test('commit sha already exists for another subproject - dont overwrite', async () => { - const { projectId, apiKey } = await createTestProject(); + const { projectId, apiKey } = await createTestProjectWithApiKey(); const commitSha = generateRandomString(8); const originalRecord = await createCommitRecord(projectId, { @@ -437,7 +437,7 @@ describe('commit records routes', () => { describe('get commit record with base', () => { test('unknown compareTo', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const recordInDB = await createCommitRecord(projectId, { branch: 'test', @@ -455,7 +455,7 @@ describe('commit records routes', () => { }); test('no records in current branch', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const response = await app.inject({ method: 'GET', @@ -467,7 +467,7 @@ describe('commit records routes', () => { describe('without base branch', () => { test('no other records in current branch', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const recordInDB = await createCommitRecord(projectId, { branch: 'test', @@ -491,7 +491,7 @@ describe('commit records routes', () => { }); test('without older records in current branch', async () => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); const recordInDB = await createCommitRecord(projectId, { branch: 'test', @@ -524,7 +524,7 @@ describe('commit records routes', () => { test.each([{ compareTo: BaseRecordCompareTo.LatestCommit }, { compareTo: BaseRecordCompareTo.PreviousCommit }])( 'with older records in current branch, compareTo: $compareTo', async ({ compareTo }) => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); await createCommitRecord(projectId, { branch: 'test', @@ -584,7 +584,7 @@ describe('commit records routes', () => { { name: 'base branch not found, with sub project', baseBranch: 'new', subProject: 'website2' }, { name: 'base branch has commit records, with sub project', baseBranch: 'main', subProject: 'website2' }, ])('$name', async ({ baseBranch, subProject, compareTo }) => { - const { projectId } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); await createCommitRecord(projectId, { branch: 'main', diff --git a/service/src/routes/api/__tests__/projectsRoutes.spec.ts b/service/src/routes/api/__tests__/projectsRoutes.spec.ts index b324ac8..ca9bde5 100644 --- a/service/src/routes/api/__tests__/projectsRoutes.spec.ts +++ b/service/src/routes/api/__tests__/projectsRoutes.spec.ts @@ -1,8 +1,10 @@ import { ObjectId } from 'mongodb'; -import { CreateProjectResponse } from 'bundlemon-utils'; +import { CreateProjectResponse, ProjectProvider } from 'bundlemon-utils'; import { app } from '@tests/app'; -import { getProjectsCollection } from '../../../framework/mongo'; +import { getProjectsCollection } from '../../../framework/mongo/projects'; import { verifyHash } from '../../../utils/hashUtils'; +import { generateRandomString } from '@tests/utils'; +import { createTestGitProject } from '@tests/projectUtils'; describe('projects routes', () => { test('create project', async () => { @@ -26,8 +28,91 @@ describe('projects routes', () => { throw Error('project should be in DB'); } + if (!('apiKey' in projectInDb)) { + throw Error('project should have apiKey'); + } + expect(projectInDb._id.toHexString()).toEqual(responseJson.projectId); expect(responseJson.apiKey.startsWith(projectInDb.apiKey.startKey)).toBeTruthy(); expect(verifyHash(responseJson.apiKey, projectInDb.apiKey.hash)).toBeTruthy(); }); + + describe('get or create project', () => { + test('project doesnt exist', async () => { + const provider = ProjectProvider.GitHub; + const owner = generateRandomString() + 'A.a'; + const repo = generateRandomString() + 'B_-'; + const response = await app.inject({ + method: 'POST', + url: '/v1/projects/id', + payload: { + provider, + owner, + repo, + }, + }); + + expect(response.statusCode).toEqual(200); + + const responseJson = response.json<{ id: string }>(); + + expect(responseJson.id).toBeDefined(); + + const projectsCollection = await getProjectsCollection(); + const projectInDb = await projectsCollection.findOne({ _id: new ObjectId(responseJson.id) }); + + if (!projectInDb) { + throw Error('project should be in DB'); + } + + if (!('provider' in projectInDb)) { + // TODO: typescript + throw Error('project should have provider'); + } + + expect(projectInDb._id.toHexString()).toEqual(responseJson.id); + expect(projectInDb.provider).toEqual(provider); + expect(projectInDb.owner).toEqual(owner.toLowerCase()); + expect(projectInDb.repo).toEqual(repo.toLowerCase()); + expect(projectInDb.lastAccessed.getTime()).toEqual(projectInDb.creationDate.getTime()); + }); + + test('project exist', async () => { + const project = await createTestGitProject(); + + const response = await app.inject({ + method: 'POST', + url: '/v1/projects/id', + payload: { + provider: project.provider, + owner: project.owner, + repo: project.repo, + }, + }); + + expect(response.statusCode).toEqual(200); + + const responseJson = response.json<{ id: string }>(); + + expect(responseJson.id).toEqual(project.id); + + const projectsCollection = await getProjectsCollection(); + const projectInDb = await projectsCollection.findOne({ _id: new ObjectId(responseJson.id) }); + + if (!projectInDb) { + throw Error('project should be in DB'); + } + + if (!('provider' in projectInDb)) { + // TODO: typescript + throw Error('project should have provider'); + } + + expect(projectInDb._id.toHexString()).toEqual(responseJson.id); + expect(projectInDb.provider).toEqual(project.provider); + expect(projectInDb.owner).toEqual(project.owner); + expect(projectInDb.repo).toEqual(project.repo); + expect(projectInDb.lastAccessed.getTime()).toBeGreaterThan(projectInDb.creationDate.getTime()); + }); + }); }); diff --git a/service/src/routes/api/__tests__/subprojectsRoutes.spec.ts b/service/src/routes/api/__tests__/subprojectsRoutes.spec.ts index 0d2c09b..8826d5b 100644 --- a/service/src/routes/api/__tests__/subprojectsRoutes.spec.ts +++ b/service/src/routes/api/__tests__/subprojectsRoutes.spec.ts @@ -1,8 +1,8 @@ import { Compression } from 'bundlemon-utils'; import { app } from '@tests/app'; -import { createTestProject } from '@tests/projectUtils'; +import { createTestProjectWithApiKey } from '@tests/projectUtils'; import { generateRandomString } from '@tests/utils'; -import { createCommitRecord } from '../../../framework/mongo'; +import { createCommitRecord } from '../../../framework/mongo/commitRecords'; describe('sub projects routes', () => { describe('get sub projects', () => { @@ -22,8 +22,8 @@ describe('sub projects routes', () => { }); test('no sub projects', async () => { - const { projectId } = await createTestProject(); - const { projectId: projectId2 } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); + const { projectId: projectId2 } = await createTestProjectWithApiKey(); await createCommitRecord(projectId, { branch: 'other', @@ -62,8 +62,8 @@ describe('sub projects routes', () => { }); test('with sub projects', async () => { - const { projectId } = await createTestProject(); - const { projectId: projectId2 } = await createTestProject(); + const { projectId } = await createTestProjectWithApiKey(); + const { projectId: projectId2 } = await createTestProjectWithApiKey(); await createCommitRecord(projectId, { branch: 'other', diff --git a/service/src/routes/api/v1.ts b/service/src/routes/api/v1.ts index 0ff006f..6e1de21 100644 --- a/service/src/routes/api/v1.ts +++ b/service/src/routes/api/v1.ts @@ -3,7 +3,7 @@ import { getCommitRecordsController, getCommitRecordWithBaseController, } from '../../controllers/commitRecordsController'; -import { createProjectController } from '../../controllers/projectsController'; +import { createProjectController, getOrCreateProjectIdController } from '../../controllers/projectsController'; import { CreateCommitRecordRequestSchema, GetCommitRecordsRequestSchema, @@ -11,8 +11,9 @@ import { CreateGithubCheckRequestSchema, CreateGithubCommitStatusRequestSchema, PostGithubPRCommentRequestSchema, - GithubOutputRequestSchema, + LegacyGithubOutputRequestSchema, GetSubprojectsRequestSchema, + GetOrCreateProjectIdRequestSchema, } from '../../consts/schemas'; import type { FastifyPluginCallback } from 'fastify'; @@ -20,8 +21,8 @@ import { createGithubCheckController, createGithubCommitStatusController, postGithubPRCommentController, - githubOutputController, -} from '../../controllers/githubController'; + legacyGithubOutputController, +} from '../../controllers/legacyGithubController'; import { getSubprojectsController } from '../../controllers/subprojectsController'; const commitRecordRoutes: FastifyPluginCallback = (app, _opts, done) => { @@ -41,7 +42,7 @@ const commitRecordsRoutes: FastifyPluginCallback = (app, _opts, done) => { const outputsRoutes: FastifyPluginCallback = (app, _opts, done) => { // bundlemon > v0.4.0 - app.post('/github', { schema: GithubOutputRequestSchema.properties }, githubOutputController); + app.post('/github', { schema: LegacyGithubOutputRequestSchema.properties }, legacyGithubOutputController); // bundlemon <= v0.4.0 app.post('/github/check-run', { schema: CreateGithubCheckRequestSchema.properties }, createGithubCheckController); @@ -75,6 +76,7 @@ const projectRoutes: FastifyPluginCallback = (app, _opts, done) => { const projectsRoutes: FastifyPluginCallback = (app, _opts, done) => { app.post('/', createProjectController); + app.post('/id', { schema: GetOrCreateProjectIdRequestSchema.properties }, getOrCreateProjectIdController); app.register(projectRoutes, { prefix: '/:projectId' }); diff --git a/service/src/types.ts b/service/src/types.ts index e6edc1c..baf2650 100644 --- a/service/src/types.ts +++ b/service/src/types.ts @@ -1,3 +1,5 @@ +import type { ProjectProvider } from 'bundlemon-utils'; + export interface ProjectApiKey { hash: string; startKey: string; @@ -8,3 +10,19 @@ export interface Project { creationDate: string; apiKey: ProjectApiKey; } + +export interface GitDetails { + provider: ProjectProvider; + /** + * @minLength 1 + * @maxLength 100 + * @pattern ^[a-zA-Z0-9_.-]*$ + */ + owner: string; + /** + * @minLength 1 + * @maxLength 100 + * @pattern ^[a-zA-Z0-9_.-]*$ + */ + repo: string; +} diff --git a/service/src/types/schemas/commitRecords.ts b/service/src/types/schemas/commitRecords.ts index f30a9a7..854909f 100644 --- a/service/src/types/schemas/commitRecords.ts +++ b/service/src/types/schemas/commitRecords.ts @@ -1,16 +1,28 @@ /* istanbul ignore file */ import type { CommitRecordPayload } from 'bundlemon-utils'; -import type { CommitRecordsQueryResolution, BaseRecordCompareTo } from '../../consts/commitRecords'; +import type { + CommitRecordsQueryResolution, + BaseRecordCompareTo, + CreateCommitRecordAuthType, +} from '../../consts/commitRecords'; import type { BaseRequestSchema, BaseGetRequestSchema, AuthHeaders, ProjectIdParams } from './common'; +export type CreateCommitRecordGithubActionsAuthQuery = { + authType: CreateCommitRecordAuthType.GithubActions; + runId: string; +}; + +export type CreateCommitRecordRequestQuery = Record | CreateCommitRecordGithubActionsAuthQuery; + export interface CreateCommitRecordRequestSchema extends BaseRequestSchema { body: CommitRecordPayload; params: ProjectIdParams; + query: CreateCommitRecordRequestQuery; headers: AuthHeaders; } -interface GetCommitRecordRequestParams extends ProjectIdParams { +export interface GetCommitRecordRequestParams extends ProjectIdParams { /** * @pattern ^[0-9a-fA-F]{24}$ */ diff --git a/service/src/types/schemas/common.ts b/service/src/types/schemas/common.ts index ffbe084..ac7b1f7 100644 --- a/service/src/types/schemas/common.ts +++ b/service/src/types/schemas/common.ts @@ -40,6 +40,7 @@ export interface ProjectAuthHeaders { 'x-api-key': string; } +// @deprecated export interface GithubActionsAuthHeaders { 'bundlemon-auth-type': 'GITHUB_ACTION'; /** @@ -57,6 +58,13 @@ export interface GithubActionsAuthHeaders { 'github-run-id': string; } +export interface AuthorizationHeader { + /** + * @minLength 1 + */ + authorization: string; +} + export type AuthHeaders = { [key: string]: any } & (ProjectAuthHeaders | GithubActionsAuthHeaders); export interface ProjectIdParams { diff --git a/service/src/types/schemas/githubOutput.ts b/service/src/types/schemas/githubOutput.ts index f86cb07..9139066 100644 --- a/service/src/types/schemas/githubOutput.ts +++ b/service/src/types/schemas/githubOutput.ts @@ -1,6 +1,7 @@ /* istanbul ignore file */ import type { GithubOutputTypes, Report } from 'bundlemon-utils'; +import type { GetCommitRecordRequestParams } from './commitRecords'; import type { AuthHeaders, BaseRequestSchema } from './common'; interface ProjectIdParams { @@ -62,7 +63,7 @@ export interface PostGithubPRCommentRequestSchema extends BaseRequestSchema { headers: ProjectApiKeyHeaders; } -interface GithubOutputBody { +interface LegacyGithubOutputBody { report: Report; git: { owner: string; @@ -73,8 +74,23 @@ interface GithubOutputBody { output: Partial>; } -export interface GithubOutputRequestSchema extends BaseRequestSchema { - body: GithubOutputBody; +export interface LegacyGithubOutputRequestSchema extends BaseRequestSchema { + body: LegacyGithubOutputBody; params: ProjectIdParams; headers: AuthHeaders; } + +interface GithubOutputBody { + git: { + owner: string; + repo: string; + commitSha: string; + prNumber?: string; + } & ({ token: string } | { runId: string }); + output: Partial>; +} + +export interface GithubOutputRequestSchema extends BaseRequestSchema { + body: GithubOutputBody; + params: GetCommitRecordRequestParams; +} diff --git a/service/src/types/schemas/projects.ts b/service/src/types/schemas/projects.ts new file mode 100644 index 0000000..5f608f6 --- /dev/null +++ b/service/src/types/schemas/projects.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ + +import { GitDetails } from '../../types'; +import type { BaseRequestSchema } from './common'; + +export interface GetOrCreateProjectIdRequestSchema extends BaseRequestSchema { + body: GitDetails; +} diff --git a/service/src/utils/projectUtils.ts b/service/src/utils/projectUtils.ts new file mode 100644 index 0000000..4d1c648 --- /dev/null +++ b/service/src/utils/projectUtils.ts @@ -0,0 +1,18 @@ +import { ProjectProvider } from 'bundlemon-utils'; +import { Project, GitProject } from '../framework/mongo/projects'; +import type { FastifyLoggerInstance } from 'fastify'; + +export function isGitHubProject(project: Project, log: FastifyLoggerInstance): project is GitProject { + if (!('provider' in project && 'owner' in project && 'repo' in project)) { + log.warn({ projectId: project.id }, 'project missing provider details'); + return false; + } + const { provider } = project; + + if (provider !== ProjectProvider.GitHub) { + log.warn({ projectId: project.id }, 'project provider is not GitHub'); + return false; + } + + return true; +} diff --git a/service/src/utils/reportUtils.ts b/service/src/utils/reportUtils.ts new file mode 100644 index 0000000..34d55b4 --- /dev/null +++ b/service/src/utils/reportUtils.ts @@ -0,0 +1,17 @@ +import { generateDiffReport, Report } from 'bundlemon-utils'; +import type { CommitRecordWithBase } from '../framework/mongo/commitRecords'; +import { generateLinkToReport } from './linkUtils'; + +export function generateReport({ record, baseRecord }: CommitRecordWithBase): Report { + const diffReport = generateDiffReport(record, baseRecord); + + return { + ...diffReport, + metadata: { + subProject: record.subProject, + linkToReport: generateLinkToReport({ projectId: record.projectId, commitRecordId: record.id }), + record, + baseRecord, + }, + }; +} diff --git a/service/tests/projectUtils.ts b/service/tests/projectUtils.ts index a375591..0d5f0e8 100644 --- a/service/tests/projectUtils.ts +++ b/service/tests/projectUtils.ts @@ -1,8 +1,10 @@ import { randomBytes } from 'crypto'; import { createHash } from '../src/utils/hashUtils'; -import { createProject } from '../src/framework/mongo'; +import { createProject, getProjectsCollection, GitProject, ProjectDB } from '../src/framework/mongo/projects'; +import { ProjectProvider } from 'bundlemon-utils'; +import { generateRandomString } from './utils'; -export async function createTestProject() { +export async function createTestProjectWithApiKey() { const apiKey = randomBytes(32).toString('hex'); const startKey = apiKey.substring(0, 3); @@ -11,3 +13,16 @@ export async function createTestProject() { return { projectId, apiKey }; } + +export async function createTestGitProject(): Promise { + const provider = ProjectProvider.GitHub; + const owner = generateRandomString(); + const repo = generateRandomString(); + + const newProject: ProjectDB = { provider, owner, repo, creationDate: new Date(), lastAccessed: new Date() }; + + const projectsCollection = await getProjectsCollection(); + const id = (await projectsCollection.insertOne(newProject)).insertedId; + + return { id: id.toHexString(), ...newProject }; +} diff --git a/yarn.lock b/yarn.lock index f201398..c4c23d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11150,7 +11150,12 @@ typescript@3.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== -typescript@^4.3.5, typescript@~4.3.4: +typescript@^4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== + +typescript@~4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== From f5e400c76c8615cf9420c6e838e4f7e68770f005 Mon Sep 17 00:00:00 2001 From: LironEr Date: Fri, 3 Jun 2022 13:40:58 +0300 Subject: [PATCH 2/6] fix tests --- packages/bundlemon/src/common/service.ts | 2 +- .../src/main/outputs/outputManager.ts | 4 +- service/package.json | 6 +- service/src/consts/schemas.ts | 135 +++---- service/src/controllers/utils/auth.ts | 13 +- .../api/__tests__/commitRecordsRoutes.spec.ts | 349 +++++++++++++++++- .../api/__tests__/projectsRoutes.spec.ts | 4 +- service/src/types/schemas/commitRecords.ts | 2 +- service/src/types/schemas/common.ts | 10 +- service/tests/hooks.ts | 6 +- service/tests/projectUtils.ts | 15 +- service/tests/utils.ts | 6 + yarn.lock | 339 +++++++++-------- 13 files changed, 645 insertions(+), 246 deletions(-) diff --git a/packages/bundlemon/src/common/service.ts b/packages/bundlemon/src/common/service.ts index 69d720a..a51008a 100644 --- a/packages/bundlemon/src/common/service.ts +++ b/packages/bundlemon/src/common/service.ts @@ -57,7 +57,7 @@ export async function createCommitRecord( return res.data; } catch (err) { - logError(err, 'create commit record'); + logError(err as Error, 'create commit record'); } return undefined; diff --git a/packages/bundlemon/src/main/outputs/outputManager.ts b/packages/bundlemon/src/main/outputs/outputManager.ts index 257322e..cc35479 100644 --- a/packages/bundlemon/src/main/outputs/outputManager.ts +++ b/packages/bundlemon/src/main/outputs/outputManager.ts @@ -39,7 +39,7 @@ export class OutputManager { logger.debug(`Ignoring output "${name}"`); } } catch (err) { - throw new Error(`Error while creating "${name}" output: ${err.message}`); + throw new Error(`Error while creating "${name}" output: ${(err as Error).message}`); } } } @@ -54,7 +54,7 @@ export class OutputManager { try { await instance.generate(report); } catch (err) { - throw new Error(`Error while generating "${name}" output. ${err.message}`); + throw new Error(`Error while generating "${name}" output. ${(err as Error).message}`); } } }; diff --git a/service/package.json b/service/package.json index 1eaa669..ae36d73 100644 --- a/service/package.json +++ b/service/package.json @@ -28,8 +28,8 @@ "bundlemon-markdown-output": "^0.1.1", "bytes": "^3.1.0", "env-var": "^7.0.1", - "fastify": "^3.27.0", - "@fastify/cors": "^7.0.0", + "fastify": "^4.0.0-rc.4", + "@fastify/cors": "^8.0.0", "mongodb": "^4.3.1" }, "devDependencies": { @@ -37,7 +37,7 @@ "dotenv": "^10.0.0", "nodemon": "^2.0.7", "rimraf": "^3.0.2", - "ts-json-schema-generator": "^0.94.1", + "ts-json-schema-generator": "^1.0.0", "ts-node": "^10.2.0", "vercel": "^21.3.3", "typescript": "^4.6.4" diff --git a/service/src/consts/schemas.ts b/service/src/consts/schemas.ts index 1932dba..3c58563 100644 --- a/service/src/consts/schemas.ts +++ b/service/src/consts/schemas.ts @@ -25,10 +25,6 @@ export const ProjectAuthHeaders = { $id: '#/definitions/ProjectAuthHeaders', type: 'object', properties: { - 'bundlemon-auth-type': { - type: 'string', - const: 'API_KEY', - }, 'x-api-key': { type: 'string', minLength: 1, @@ -46,13 +42,21 @@ export const GithubActionsAuthHeaders = { type: 'string', const: 'GITHUB_ACTION', }, + 'github-owner': { + type: 'string', + minLength: 1, + }, + 'github-repo': { + type: 'string', + minLength: 1, + }, 'github-run-id': { type: 'string', minLength: 1, pattern: '^\\d+$', }, }, - required: ['bundlemon-auth-type', 'github-run-id'], + required: ['bundlemon-auth-type', 'github-owner', 'github-repo', 'github-run-id'], additionalProperties: false, }; @@ -61,32 +65,12 @@ export const AuthHeaders = { anyOf: [ { type: 'object', - properties: { - 'bundlemon-auth-type': { - type: 'string', - const: 'API_KEY', - }, - 'x-api-key': { - type: 'string', - minLength: 1, - }, - }, - required: ['x-api-key'], }, { - type: 'object', - properties: { - 'bundlemon-auth-type': { - type: 'string', - const: 'GITHUB_ACTION', - }, - 'github-run-id': { - type: 'string', - minLength: 1, - pattern: '^\\d+$', - }, - }, - required: ['bundlemon-auth-type', 'github-run-id'], + $ref: '#/definitions/ProjectAuthHeaders', + }, + { + $ref: '#/definitions/GithubActionsAuthHeaders', }, ], }; @@ -104,6 +88,37 @@ export const ProjectIdParams = { additionalProperties: false, }; +export const CreateCommitRecordGithubActionsAuthQuery = { + $id: '#/definitions/CreateCommitRecordGithubActionsAuthQuery', + type: 'object', + properties: { + authType: { + type: 'string', + const: 'GITHUB_ACTIONS', + }, + runId: { + type: 'string', + }, + }, + required: ['authType', 'runId'], + additionalProperties: false, +}; + +export const CreateCommitRecordRequestQuery = { + $id: '#/definitions/CreateCommitRecordRequestQuery', + anyOf: [ + { + $ref: '#/definitions/CreateCommitRecordGithubActionsAuthQuery', + }, + { + type: 'object', + additionalProperties: { + not: {}, + }, + }, + ], +}; + export const CreateCommitRecordRequestSchema = { $id: '#/definitions/CreateCommitRecordRequestSchema', type: 'object', @@ -111,7 +126,9 @@ export const CreateCommitRecordRequestSchema = { body: { $ref: '#/definitions/CommitRecordPayload', }, - query: {}, + query: { + $ref: '#/definitions/CreateCommitRecordRequestQuery', + }, params: { $ref: '#/definitions/ProjectIdParams', }, @@ -119,7 +136,7 @@ export const CreateCommitRecordRequestSchema = { $ref: '#/definitions/AuthHeaders', }, }, - required: ['body', 'params', 'headers'], + required: ['body', 'params', 'query', 'headers'], additionalProperties: false, }; @@ -205,6 +222,23 @@ export const Compression = { enum: ['none', 'gzip', 'brotli'], }; +export const GetCommitRecordRequestParams = { + $id: '#/definitions/GetCommitRecordRequestParams', + type: 'object', + properties: { + projectId: { + type: 'string', + pattern: '^[0-9a-fA-F]{24}$', + }, + commitRecordId: { + type: 'string', + pattern: '^[0-9a-fA-F]{24}$', + }, + }, + required: ['commitRecordId', 'projectId'], + additionalProperties: false, +}; + export const GetCommitRecordRequestSchema = { $id: '#/definitions/GetCommitRecordRequestSchema', type: 'object', @@ -220,19 +254,7 @@ export const GetCommitRecordRequestSchema = { additionalProperties: false, }, params: { - type: 'object', - properties: { - projectId: { - type: 'string', - pattern: '^[0-9a-fA-F]{24}$', - }, - commitRecordId: { - type: 'string', - pattern: '^[0-9a-fA-F]{24}$', - }, - }, - required: ['commitRecordId', 'projectId'], - additionalProperties: false, + $ref: '#/definitions/GetCommitRecordRequestParams', }, headers: {}, }, @@ -820,9 +842,6 @@ export const GithubOutputRequestSchema = { body: { type: 'object', properties: { - report: { - $ref: '#/definitions/Report', - }, git: { anyOf: [ { @@ -851,7 +870,7 @@ export const GithubOutputRequestSchema = { type: 'object', additionalProperties: false, properties: { - actionId: { + runId: { type: 'string', }, owner: { @@ -867,7 +886,7 @@ export const GithubOutputRequestSchema = { type: 'string', }, }, - required: ['actionId', 'commitSha', 'owner', 'repo'], + required: ['commitSha', 'owner', 'repo', 'runId'], }, ], }, @@ -887,26 +906,16 @@ export const GithubOutputRequestSchema = { additionalProperties: false, }, }, - required: ['report', 'git', 'output'], + required: ['git', 'output'], additionalProperties: false, }, query: {}, params: { - type: 'object', - properties: { - projectId: { - type: 'string', - pattern: '^[0-9a-fA-F]{24}$', - }, - }, - required: ['projectId'], - additionalProperties: false, - }, - headers: { - $ref: '#/definitions/AuthHeaders', + $ref: '#/definitions/GetCommitRecordRequestParams', }, + headers: {}, }, - required: ['body', 'params', 'headers'], + required: ['body', 'params'], additionalProperties: false, }; diff --git a/service/src/controllers/utils/auth.ts b/service/src/controllers/utils/auth.ts index 1cfdeed..b8209c8 100644 --- a/service/src/controllers/utils/auth.ts +++ b/service/src/controllers/utils/auth.ts @@ -19,7 +19,7 @@ type CheckAuthResponse = export async function checkAuth( projectId: string, headers: AuthHeaders, - query: CreateCommitRecordRequestQuery | Record, + query: CreateCommitRecordRequestQuery, commitSha: string | undefined, log: FastifyLoggerInstance ): Promise { @@ -36,7 +36,7 @@ export async function checkAuth( // deprecated if (headers['bundlemon-auth-type'] === 'GITHUB_ACTION') { - return handleLegacyGithubActionAuth(project, headers, log); + return handleLegacyGithubActionAuth(project, headers as GithubActionsAuthHeaders, log); } if ('authType' in query && query.authType === CreateCommitRecordAuthType.GithubActions) { @@ -73,11 +73,16 @@ async function handleLegacyGithubActionAuth( headers: GithubActionsAuthHeaders, log: FastifyLoggerInstance ): Promise { - const { 'github-owner': owner, 'github-repo': repo, 'github-run-id': runId } = headers as GithubActionsAuthHeaders; + const { 'github-owner': owner, 'github-repo': repo, 'github-run-id': runId } = headers; + + if (!owner || !repo || !runId) { + log.warn({ projectId: project.id }, 'legacy github auth: empty params'); + return { authenticated: false, error: 'forbidden' }; + } if ('provider' in project) { log.warn({ projectId: project.id }, 'legacy github auth works only with old projects'); - return { authenticated: false, error: 'forbidden' }; + return { authenticated: false, error: 'legacy github auth works only with old projects' }; } return createOctokitClientByAction({ owner, repo, runId }, log); diff --git a/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts b/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts index 8f8fba3..5080e49 100644 --- a/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts +++ b/service/src/routes/api/__tests__/commitRecordsRoutes.spec.ts @@ -1,3 +1,4 @@ +import { mocked } from 'ts-jest/utils'; import { ObjectId } from 'mongodb'; import { URLSearchParams } from 'url'; import { @@ -8,13 +9,20 @@ import { BaseCommitRecordResponse, } from 'bundlemon-utils'; import { app } from '@tests/app'; -import { createTestProjectWithApiKey } from '@tests/projectUtils'; -import { generateRandomString } from '@tests/utils'; +import { createTestGithubProject, createTestProjectWithApiKey, generateProjectId } from '@tests/projectUtils'; +import { generateRandomInt, generateRandomString } from '@tests/utils'; import { createCommitRecord, getCommitRecordsCollection } from '../../../framework/mongo/commitRecords'; import { generateLinkToReport } from '../../../utils/linkUtils'; -import { BaseRecordCompareTo } from '../../..//consts/commitRecords'; +import { BaseRecordCompareTo, CreateCommitRecordAuthType } from '../../../consts/commitRecords'; +import { createOctokitClientByAction } from '../../../framework/github'; + +jest.mock('../../../framework/github'); describe('commit records routes', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + describe('get commit records', () => { test('without branch', async () => { const { projectId } = await createTestProjectWithApiKey(); @@ -122,7 +130,7 @@ describe('commit records routes', () => { }); describe('create commit record', () => { - test('not authenticated', async () => { + test('bad API key', async () => { const { projectId } = await createTestProjectWithApiKey(); const payload: CommitRecordPayload = { @@ -145,6 +153,339 @@ describe('commit records routes', () => { expect(response.statusCode).toEqual(403); }); + test('project not found', async () => { + const projectId = generateProjectId(); + + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${projectId}/commit-records`, + payload, + headers: { + 'x-api-key': 'api-key', + }, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('forbidden'); + }); + + test('unknown auth type', async () => { + const { projectId } = await createTestProjectWithApiKey(); + + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${projectId}/commit-records`, + payload, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('forbidden'); + }); + + test('project without api key', async () => { + const project = await createTestGithubProject(); + + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.id}/commit-records`, + payload, + headers: { + 'x-api-key': 'api-key', + }, + }); + + expect(response.statusCode).toEqual(403); + }); + + describe('GitHub auth', () => { + test('success', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction).mockResolvedValue({ + authenticated: true, + installationOctokit: {} as any, + }); + const project = await createTestGithubProject(); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.id}/commit-records`, + payload, + query: { + authType: CreateCommitRecordAuthType.GithubActions, + runId, + }, + }); + + const responseJson = response.json(); + const { record } = responseJson; + + expect(response.statusCode).toEqual(200); + expect(mockedCreateOctokitClientByAction).toHaveBeenCalledWith( + { + owner: project.owner, + repo: project.repo, + commitSha: payload.commitSha, + runId, + }, + expect.any(Object) + ); + + // Validate the record exist in the DB + const commitRecordsCollection = await getCommitRecordsCollection(); + const recordInDb = await commitRecordsCollection.findOne({ _id: new ObjectId(record.id) }); + + expect(recordInDb).toBeDefined(); + }); + + test('not authenticated', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction).mockResolvedValue({ + authenticated: false, + error: 'message from github', + }); + const project = await createTestGithubProject(); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.id}/commit-records`, + payload, + query: { + authType: CreateCommitRecordAuthType.GithubActions, + runId, + }, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('message from github'); + expect(mockedCreateOctokitClientByAction).toHaveBeenCalledWith( + { + owner: project.owner, + repo: project.repo, + commitSha: payload.commitSha, + runId, + }, + expect.any(Object) + ); + }); + + test('not authenticated: other provider project', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction); + // @ts-expect-error + const project = await createTestGithubProject({ provider: 'travis' }); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.id}/commit-records`, + payload, + query: { + authType: CreateCommitRecordAuthType.GithubActions, + runId, + }, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('forbidden'); + expect(mockedCreateOctokitClientByAction).toHaveBeenCalledTimes(0); + }); + + test('not authenticated: project with API key', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction); + const project = await createTestProjectWithApiKey(); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.projectId}/commit-records`, + payload, + query: { + authType: CreateCommitRecordAuthType.GithubActions, + runId, + }, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('forbidden'); + expect(mockedCreateOctokitClientByAction).toHaveBeenCalledTimes(0); + }); + describe('legacy auth', () => { + test('success', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction).mockResolvedValue({ + authenticated: true, + installationOctokit: {} as any, + }); + const project = await createTestProjectWithApiKey(); + const owner = generateRandomString(); + const repo = generateRandomString(); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.projectId}/commit-records`, + payload, + headers: { + 'bundlemon-auth-type': 'GITHUB_ACTION', + 'github-owner': owner, + 'github-repo': repo, + 'github-run-id': runId, + }, + }); + + const responseJson = response.json(); + const { record } = responseJson; + + expect(response.statusCode).toEqual(200); + expect(mockedCreateOctokitClientByAction).toHaveBeenCalledWith( + { + owner, + repo, + runId, + }, + expect.any(Object) + ); + + // Validate the record exist in the DB + const commitRecordsCollection = await getCommitRecordsCollection(); + const recordInDb = await commitRecordsCollection.findOne({ _id: new ObjectId(record.id) }); + + expect(recordInDb).toBeDefined(); + }); + + test('not authenticated', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction).mockResolvedValue({ + authenticated: false, + error: 'message from github', + }); + const project = await createTestProjectWithApiKey(); + const owner = generateRandomString(); + const repo = generateRandomString(); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.projectId}/commit-records`, + payload, + headers: { + 'bundlemon-auth-type': 'GITHUB_ACTION', + 'github-owner': owner, + 'github-repo': repo, + 'github-run-id': runId, + }, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('message from github'); + expect(mockedCreateOctokitClientByAction).toHaveBeenCalledWith( + { + owner, + repo, + runId, + }, + expect.any(Object) + ); + }); + + test('not authenticated - git project', async () => { + const mockedCreateOctokitClientByAction = mocked(createOctokitClientByAction); + const project = await createTestGithubProject(); + const runId = String(generateRandomInt(1000000, 99999999)); + const payload: CommitRecordPayload = { + branch: 'test', + commitSha: generateRandomString(8), + files: [{ path: 'file.js', pattern: '*.js', size: 100, compression: Compression.None }], + groups: [], + }; + + const response = await app.inject({ + method: 'POST', + url: `/v1/projects/${project.id}/commit-records`, + payload, + headers: { + 'bundlemon-auth-type': 'GITHUB_ACTION', + 'github-owner': project.owner, + 'github-repo': project.repo, + 'github-run-id': runId, + }, + }); + + const responseJson = response.json(); + + expect(response.statusCode).toEqual(403); + expect(responseJson.error).toBe('legacy github auth works only with old projects'); + expect(mockedCreateOctokitClientByAction).toBeCalledTimes(0); + }); + }); + }); + describe('without base branch', () => { test('no records in current branch', async () => { const { projectId, apiKey } = await createTestProjectWithApiKey(); diff --git a/service/src/routes/api/__tests__/projectsRoutes.spec.ts b/service/src/routes/api/__tests__/projectsRoutes.spec.ts index ca9bde5..4460de3 100644 --- a/service/src/routes/api/__tests__/projectsRoutes.spec.ts +++ b/service/src/routes/api/__tests__/projectsRoutes.spec.ts @@ -4,7 +4,7 @@ import { app } from '@tests/app'; import { getProjectsCollection } from '../../../framework/mongo/projects'; import { verifyHash } from '../../../utils/hashUtils'; import { generateRandomString } from '@tests/utils'; -import { createTestGitProject } from '@tests/projectUtils'; +import { createTestGithubProject } from '@tests/projectUtils'; describe('projects routes', () => { test('create project', async () => { @@ -78,7 +78,7 @@ describe('projects routes', () => { }); test('project exist', async () => { - const project = await createTestGitProject(); + const project = await createTestGithubProject(); const response = await app.inject({ method: 'POST', diff --git a/service/src/types/schemas/commitRecords.ts b/service/src/types/schemas/commitRecords.ts index 854909f..d2bd0ce 100644 --- a/service/src/types/schemas/commitRecords.ts +++ b/service/src/types/schemas/commitRecords.ts @@ -13,7 +13,7 @@ export type CreateCommitRecordGithubActionsAuthQuery = { runId: string; }; -export type CreateCommitRecordRequestQuery = Record | CreateCommitRecordGithubActionsAuthQuery; +export type CreateCommitRecordRequestQuery = CreateCommitRecordGithubActionsAuthQuery | Record; export interface CreateCommitRecordRequestSchema extends BaseRequestSchema { body: CommitRecordPayload; diff --git a/service/src/types/schemas/common.ts b/service/src/types/schemas/common.ts index ac7b1f7..8e1a770 100644 --- a/service/src/types/schemas/common.ts +++ b/service/src/types/schemas/common.ts @@ -33,7 +33,6 @@ export type FastifyValidatedRoute = Rout >; export interface ProjectAuthHeaders { - 'bundlemon-auth-type'?: 'API_KEY'; /** * @minLength 1 */ @@ -58,14 +57,7 @@ export interface GithubActionsAuthHeaders { 'github-run-id': string; } -export interface AuthorizationHeader { - /** - * @minLength 1 - */ - authorization: string; -} - -export type AuthHeaders = { [key: string]: any } & (ProjectAuthHeaders | GithubActionsAuthHeaders); +export type AuthHeaders = Record | ProjectAuthHeaders | GithubActionsAuthHeaders; export interface ProjectIdParams { /** diff --git a/service/tests/hooks.ts b/service/tests/hooks.ts index 03e696f..bae96e9 100644 --- a/service/tests/hooks.ts +++ b/service/tests/hooks.ts @@ -1,9 +1,13 @@ -import { app } from './app'; +/* eslint-disable @typescript-eslint/no-var-requires */ + +// When using import on top of the file mocks wont mock, so use require inside the hooks beforeAll(async () => { + const { app } = require('./app'); await app.ready(); }); afterAll(async () => { + const { app } = require('./app'); await app.close(); }); diff --git a/service/tests/projectUtils.ts b/service/tests/projectUtils.ts index 0d5f0e8..6712c36 100644 --- a/service/tests/projectUtils.ts +++ b/service/tests/projectUtils.ts @@ -14,15 +14,26 @@ export async function createTestProjectWithApiKey() { return { projectId, apiKey }; } -export async function createTestGitProject(): Promise { +export async function createTestGithubProject(overrides: Partial = {}): Promise { const provider = ProjectProvider.GitHub; const owner = generateRandomString(); const repo = generateRandomString(); - const newProject: ProjectDB = { provider, owner, repo, creationDate: new Date(), lastAccessed: new Date() }; + const newProject: ProjectDB = { + provider, + owner, + repo, + creationDate: new Date(), + lastAccessed: new Date(), + ...overrides, + }; const projectsCollection = await getProjectsCollection(); const id = (await projectsCollection.insertOne(newProject)).insertedId; return { id: id.toHexString(), ...newProject }; } + +export function generateProjectId() { + return generateRandomString(24); +} diff --git a/service/tests/utils.ts b/service/tests/utils.ts index 3f8a7f4..ac66079 100644 --- a/service/tests/utils.ts +++ b/service/tests/utils.ts @@ -3,3 +3,9 @@ import { randomBytes } from 'crypto'; export function generateRandomString(length = 10) { return randomBytes(length / 2).toString('hex'); } + +export function generateRandomInt(min: number, max: number) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min) + min); +} diff --git a/yarn.lock b/yarn.lock index c4c23d9..14c469a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1189,21 +1189,35 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@fastify/ajv-compiler@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz#5ce80b1fc8bebffc8c5ba428d5e392d0f9ed10a1" - integrity sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg== +"@fastify/ajv-compiler@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-3.1.0.tgz#7ccae63da5a115f583ae6cc68173dbc3c1f34405" + integrity sha512-+hRMMxcUmdqtnCGPwrI2yczFdlgp3IBR88WlPLimXlgRb8vHBTXz38I17R/9ui+hIt9jx0uOdZKOis77VooHfA== dependencies: - ajv "^6.12.6" + ajv "^8.10.0" + ajv-formats "^2.1.1" + fast-uri "^1.0.1" -"@fastify/cors@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-7.0.0.tgz#c67c5a5909498b696bb19578e903f36037ac6f32" - integrity sha512-nlo6ScwagBNJacAZD3KX90xjWLIoV0vN9QqoX1wUE9ZeZMdvkVkMZCGlxEtr00NshV0X5wDge4w5rwox7rRzSg== +"@fastify/cors@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-8.0.0.tgz#0b1f6559dad4b4291a4413ffb5b7ace760aad19d" + integrity sha512-mB2GsA7aVwq7XG6B2OM1FMpcaiXY69ZbM1h/xDJxLEVu5ITGcs5XYrBIYTMNU2dQtzO6mzXhGd2dEKaCnB7UgQ== dependencies: fastify-plugin "^3.0.0" vary "^1.1.2" +"@fastify/error@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.0.0.tgz#bfcb7b33cec0196413083a91ef2edc7b2c88455b" + integrity sha512-dPRyT40GiHRzSCll3/Jn2nPe25+E1VXc9tDwRAIKwFCxd5Np5wzgz1tmooWG3sV0qKgrBibihVoCna2ru4SEFg== + +"@fastify/fast-json-stringify-compiler@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-2.0.0.tgz#caac6f9e42bf83ecb372b8f7c2d1d3a62465e984" + integrity sha512-zRYRdzh4lEWwwLih1IaQCI7VtXAAqllAK4SGt62Q/phIZ208FniTXlw2qP0VI7JGSS+pw+SpwLT192SoiD0RCg== + dependencies: + fast-json-stringify "^3.0.0" + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -3334,7 +3348,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abstract-logging@^2.0.0: +abstract-logging@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== @@ -3433,7 +3447,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.5.5: +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3443,10 +3457,10 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, aj json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: - version "8.8.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb" - integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw== +ajv@^8.0.0, ajv@^8.0.1, ajv@^8.1.0, ajv@^8.10.0, ajv@^8.8.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -3682,10 +3696,10 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -avvio@^7.1.2: - version "7.2.1" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.1.tgz#d2323bbebfe308189e5a41386de834ce3fbbd2f8" - integrity sha512-b+gox68dqD6c3S3t+bZBKN6rYbVWdwpN12sHQLFTiacDT2rcq7fm07Ww+IKt/AvAkyCIe1f5ArP1bC/vAlx97A== +avvio@^8.1.0: + version "8.1.3" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.1.3.tgz#9ff0839ade93bcab62e212e7bdd4281dd6adccc3" + integrity sha512-tl9TC0yDRKzP6gFLkrInqPyx8AkfBC/0QRnwkE9Jo31+OJjLrE/73GJuE0QgSB0Vpv38CTJJZGqU9hczowclWw== dependencies: archy "^1.0.0" debug "^4.0.0" @@ -4400,6 +4414,11 @@ commander@^8.0.0, commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@^9.0.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.3.0.tgz#f619114a5a2d2054e0d9ff1b31d5ccf89255e26b" + integrity sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -4590,10 +4609,10 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== copy-webpack-plugin@^10.2.4: version "10.2.4" @@ -5192,6 +5211,16 @@ duplexer@^0.1.1, duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +duplexify@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -5249,7 +5278,7 @@ encoding@^0.1.12: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5682,11 +5711,6 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-decode-uri-component@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" - integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5713,18 +5737,20 @@ fast-glob@^3.1.1, fast-glob@^3.2.7: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-json-stringify@^2.5.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.1.tgz#60b46813f771c4554ac213c1d0cfb62e48c14a5d" - integrity sha512-DInuFDVNbwjZmcZke80IB1GlMONCAc15QqRhLQCOrAyJr1tFmQsgpXCJS2dtwxLWL1PtTtOLK4itj24wEqIMWg== +fast-json-stringify@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-3.2.0.tgz#f9031dca6459e88246731268d4223af2d2932669" + integrity sha512-rz24ul6KhuhHO8pLrMPSQFKtv1leLnhPHJuLBiAIKG6UHA/XdUrojieKkOmjpCQ3XfaBA/PEBZuDv3U9tQm3MQ== dependencies: - ajv "^6.11.0" + ajv "^8.10.0" + ajv-formats "^2.1.1" deepmerge "^4.2.2" + fast-uri "^1.0.1" rfdc "^1.2.0" string-similarity "^4.0.1" @@ -5738,51 +5764,40 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.1.tgz#d6015b971e933d03529b01333ba7f22c29961e92" integrity sha512-kYpn4Y/valC9MdrISg47tZOpYBNoTXKgT9GYXFpHN/jYFs+lFkPoisY+LcBODdKVMY96ATzvzsWv+ES/4Kmufw== -fast-safe-stringify@^2.0.8: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" - integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-uri@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-1.0.1.tgz#dd637f093bccf17ebea58a70c178ee8a70b5aa45" + integrity sha512-dbO/+ny6lX4tt7pvfPMTiHfQVR5igYKFa5BJ2a21TWuOgd2ySp5DYswsEGuMcJZLL3/eJ/MQJ5KNcXyNUvDt8w== fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== -fastify-error@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.1.tgz#8eb993e15e3cf57f0357fc452af9290f1c1278d2" - integrity sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ== - fastify-plugin@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca" integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== -fastify-warning@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" - integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw== - -fastify@^3.27.0: - version "3.27.0" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.27.0.tgz#636d5ece1a8ea81648270b60853e4d1f610fee15" - integrity sha512-p99Fd7xt4DFew39U5Wnp/Soy7jkpxpaqToekwQ3XWv+ECUPXd6bSF9l79EiwkutWALtEU/JiRlzS9qjP2gLHFg== - dependencies: - "@fastify/ajv-compiler" "^1.0.0" - abstract-logging "^2.0.0" - avvio "^7.1.2" - fast-json-stringify "^2.5.2" - fastify-error "^0.3.0" - find-my-way "^4.5.0" - flatstr "^1.0.12" - light-my-request "^4.2.0" - pino "^6.13.0" - process-warning "^1.0.0" +fastify@^4.0.0-rc.4: + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-4.0.0-rc.4.tgz#bf3db7e14e0843ca02f34bd09b54f99c21d9adc3" + integrity sha512-AJpqYgT2ZIx9JSVdruGcHlIyGK3jKPlXGqBvTUAPxhlPjhgsHDRtpvusBxKFeWSCkMDYS8g+ybaaH8JpzdbG7g== + dependencies: + "@fastify/ajv-compiler" "^3.1.0" + "@fastify/error" "^3.0.0" + "@fastify/fast-json-stringify-compiler" "^2.0.0" + abstract-logging "^2.0.1" + avvio "^8.1.0" + find-my-way v6.2.0 + light-my-request "^5.0.0" + pino "^7.5.1" + process-warning "^2.0.0" proxy-addr "^2.0.7" rfdc "^1.1.4" secure-json-parse "^2.0.0" semver "^7.3.2" - tiny-lru "^7.0.0" + tiny-lru "^8.0.1" fastq@^1.6.0, fastq@^1.6.1: version "1.11.0" @@ -5848,15 +5863,13 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-my-way@^4.5.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-4.5.1.tgz#758e959194b90aea0270db18fff75e2fceb2239f" - integrity sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg== +find-my-way@v6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-6.2.0.tgz#2b9b616118525f9e984289ad1974cd0504b9b0cf" + integrity sha512-bwkVP7SvGLnk1n92/HmUS4iqnPj1uYwK4eh/uVho4IEWeJh5MXmfSk8RzftOodUFcVvPoY8q9elIc9I6qhPSHA== dependencies: - fast-decode-uri-component "^1.0.1" fast-deep-equal "^3.1.3" safe-regex2 "^2.0.0" - semver-store "^0.3.0" find-root@^1.1.0: version "1.1.0" @@ -5886,11 +5899,6 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flatstr@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" - integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== - flatted@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" @@ -6161,15 +6169,15 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" @@ -7570,24 +7578,15 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@2.x, json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" +json5@2.x, json5@^2.1.2, json5@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== json5@^1.0.1: version "1.0.1" @@ -7605,11 +7604,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -7756,15 +7750,14 @@ libnpmpublish@^4.0.0: semver "^7.1.3" ssri "^8.0.1" -light-my-request@^4.2.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.4.1.tgz#bfa2220316eef4f6465bf2f0667780da6b7f6a71" - integrity sha512-FDNRF2mYjthIRWE7O8d/X7AzDx4otQHl4/QXbu3Q/FRwBFcgb+ZoDaUd5HwN53uQXLAiw76osN+Va0NEaOW6rQ== +light-my-request@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.0.0.tgz#2ac329d472c5c74c74be62fb2a8790c444c22ab0" + integrity sha512-0OPHKV+uHgBOnRokzL1LqeMCnSAo5l/rZS7kyB6G1I8qxGCvhXpq1M6WK565Y9A5CSn50l3DVaHnJ5FCdpguZQ== dependencies: - ajv "^6.12.2" - cookie "^0.4.0" - fastify-warning "^0.2.0" - readable-stream "^3.6.0" + ajv "^8.1.0" + cookie "^0.5.0" + process-warning "^1.0.0" set-cookie-parser "^2.4.1" lines-and-columns@^1.1.6: @@ -8224,10 +8217,10 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -8809,6 +8802,11 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +on-exit-leak-free@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" + integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -9219,23 +9217,35 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pino-std-serializers@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671" - integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg== +pino-abstract-transport@v0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0" + integrity sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ== + dependencies: + duplexify "^4.1.2" + split2 "^4.0.0" + +pino-std-serializers@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2" + integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q== -pino@^6.13.0: - version "6.13.4" - resolved "https://registry.yarnpkg.com/pino/-/pino-6.13.4.tgz#e7bd5e8292019609c841c37a3f1d73ee10bb80f7" - integrity sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA== +pino@^7.5.1: + version "7.11.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-7.11.0.tgz#0f0ea5c4683dc91388081d44bff10c83125066f6" + integrity sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg== dependencies: + atomic-sleep "^1.0.0" fast-redact "^3.0.0" - fast-safe-stringify "^2.0.8" - flatstr "^1.0.12" - pino-std-serializers "^3.1.0" + on-exit-leak-free "^0.2.0" + pino-abstract-transport v0.5.0 + pino-std-serializers "^4.0.0" process-warning "^1.0.0" quick-format-unescaped "^4.0.3" - sonic-boom "^1.0.2" + real-require "^0.1.0" + safe-stable-stringify "^2.1.0" + sonic-boom "^2.2.1" + thread-stream "^0.15.1" pirates@^4.0.1: version "4.0.1" @@ -9327,6 +9337,11 @@ process-warning@^1.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q== +process-warning@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee" + integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -9678,7 +9693,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.6.0: +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -9717,6 +9732,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381" + integrity sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg== + recharts-scale@^0.4.4: version "0.4.5" resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" @@ -10035,6 +10055,11 @@ safe-regex2@^2.0.0: dependencies: ret "~0.2.0" +safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -10119,11 +10144,6 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-store@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" - integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== - "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -10338,13 +10358,12 @@ socks@^2.3.3, socks@^2.6.1: ip "^1.1.5" smart-buffer "^4.1.0" -sonic-boom@^1.0.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" - integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg== +sonic-boom@^2.2.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611" + integrity sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg== dependencies: atomic-sleep "^1.0.0" - flatstr "^1.0.12" sort-keys@^2.0.0: version "2.0.0" @@ -10446,6 +10465,11 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" +split2@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -10497,6 +10521,11 @@ stack-utils@^2.0.3: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + string-argv@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" @@ -10853,6 +10882,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thread-stream@^0.15.1: + version "0.15.2" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4" + integrity sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA== + dependencies: + real-require "^0.1.0" + throat@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" @@ -10883,10 +10919,10 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tiny-lru@^7.0.0: - version "7.0.6" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" - integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow== +tiny-lru@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-8.0.2.tgz#812fccbe6e622ded552e3ff8a4c3b5ff34a85e4c" + integrity sha512-ApGvZ6vVvTNdsmt676grvCkUCGwzG9IqXma5Z07xJgiC5L7akUMof5U8G2JTI9Rz/ovtVhJBlY6mNhEvtjzOIg== tmp@^0.0.33: version "0.0.33" @@ -11001,17 +11037,17 @@ ts-jest@^27.0.4: semver "7.x" yargs-parser "20.x" -ts-json-schema-generator@^0.94.1: - version "0.94.1" - resolved "https://registry.yarnpkg.com/ts-json-schema-generator/-/ts-json-schema-generator-0.94.1.tgz#cfecf63cdb2bbbed30c8da2bfc74a8dfb30aa14e" - integrity sha512-v2onRxViC7Q5jAvzwfhevbWrd6EUQ/v+s2ey1RcX2oZ67ENdTumwkr434BtbW286P8z/SNmkmLE8I1AOcSbWSw== +ts-json-schema-generator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ts-json-schema-generator/-/ts-json-schema-generator-1.0.0.tgz#33e4affd1665268899eb57afbad397bc6a58cc53" + integrity sha512-F5VofsyMhNSXKII32NDS8/Ur8o2K3Sh5i/U2ke3UgCKf26ybgm2cZeT2x7VJPl1trML/9QLzz/82l0mvzmb3Vw== dependencies: - "@types/json-schema" "^7.0.7" - commander "^8.0.0" - fast-json-stable-stringify "^2.1.0" - glob "^7.1.7" - json-stable-stringify "^1.0.1" - typescript "~4.3.4" + "@types/json-schema" "^7.0.9" + commander "^9.0.0" + glob "^7.2.0" + json5 "^2.2.0" + safe-stable-stringify "^2.3.1" + typescript "~4.6.2" ts-node@8.9.1: version "8.9.1" @@ -11150,16 +11186,11 @@ typescript@3.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== -typescript@^4.6.4: +typescript@^4.6.4, typescript@~4.6.2: version "4.6.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== -typescript@~4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== - uglify-js@^3.1.4: version "3.9.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.9.4.tgz#867402377e043c1fc7b102253a22b64e5862401b" From 78b6f88141953d50c6dbf52d0fed5e3218a24695 Mon Sep 17 00:00:00 2001 From: LironEr Date: Fri, 3 Jun 2022 15:37:02 +0300 Subject: [PATCH 3/6] add new GitHub output controller --- service/src/controllers/githubController.ts | 156 +++++++----------- .../src/controllers/legacyGithubController.ts | 71 +------- service/src/controllers/projectsController.ts | 2 - service/src/controllers/utils/auth.ts | 10 +- .../src/controllers/utils/githubOutputs.ts | 89 ++++++++++ service/src/routes/api/v1.ts | 9 +- 6 files changed, 172 insertions(+), 165 deletions(-) create mode 100644 service/src/controllers/utils/githubOutputs.ts diff --git a/service/src/controllers/githubController.ts b/service/src/controllers/githubController.ts index 2e9dd8d..247c792 100644 --- a/service/src/controllers/githubController.ts +++ b/service/src/controllers/githubController.ts @@ -1,28 +1,14 @@ -import { - GithubOutputResponse, - GithubOutputTypes, - OutputResponse, - Status, - getReportConclusionText, -} from 'bundlemon-utils'; -import { - createCheck, - createCommitStatus, - createOrUpdatePRComment, - genCommentIdentifier, - createOctokitClientByToken, - createOctokitClientByAction, -} from '../framework/github'; -import { generateReportMarkdownWithLinks } from './utils/markdownReportGenerator'; -import { promiseAllObject } from '../utils/promiseUtils'; -import { getProject } from '../framework/mongo/projects'; +import { createOctokitClientByToken, createOctokitClientByAction } from '../framework/github'; +import { getProject, Project } from '../framework/mongo/projects'; import { getCommitRecordWithBase } from '../framework/mongo/commitRecords'; import { BaseRecordCompareTo } from '../consts/commitRecords'; import { generateReport } from '../utils/reportUtils'; import { isGitHubProject } from '../utils/projectUtils'; +import { createGithubOutputs } from './utils/githubOutputs'; import type { Octokit } from '@octokit/rest'; import type { FastifyValidatedRoute, GithubOutputRequestSchema } from '../types/schemas'; +import type { FastifyReply } from 'fastify'; // bundlemon > v2.0.0 export const githubOutputController: FastifyValidatedRoute = async (req, res) => { @@ -49,90 +35,16 @@ export const githubOutputController: FastifyValidatedRoute>> = {}; - - if (output.checkRun) { - const summary = generateReportMarkdownWithLinks(report); - - tasks.checkRun = createCheck({ - subProject, - owner, - repo, - commitSha, - installationOctokit: gitClient, - detailsUrl: report.metadata.linkToReport || undefined, - title: getReportConclusionText(report), - summary, - conclusion: report.status === Status.Pass ? 'success' : 'failure', - log: req.log, - }); - } - if (output.commitStatus) { - tasks.commitStatus = createCommitStatus({ - subProject, - owner, - repo, - commitSha, - installationOctokit: gitClient, - state: report.status === Status.Pass ? 'success' : 'error', - description: getReportConclusionText(report), - targetUrl: report.metadata.linkToReport || undefined, - log: req.log, - }); - } + const installationOctokit = await _createGithubClientFromRequest({ project, git, res }); - if (output.prComment) { - const title = subProject ? `BundleMon (${subProject})` : 'BundleMon'; - const body = `${genCommentIdentifier(subProject)}\n## ${title}\n${generateReportMarkdownWithLinks(report)}`; - - tasks.prComment = createOrUpdatePRComment({ - subProject, - owner, - repo, - prNumber, - installationOctokit: gitClient, - body, - log: req.log, - }); + if (!installationOctokit) { + return; } - const response: GithubOutputResponse = await promiseAllObject(tasks); + const response = await createGithubOutputs({ git, output, report, subProject, installationOctokit, log: req.log }); res.send(response); } catch (err) { @@ -144,3 +56,55 @@ export const githubOutputController: FastifyValidatedRoute { + let installationOctokit: Octokit | undefined; + + const { owner, repo, commitSha } = git; + + if ('token' in git) { + installationOctokit = createOctokitClientByToken(git.token); + } else if ('runId' in git) { + if (!isGitHubProject(project, res.log)) { + res.status(403).send({ error: 'forbidden' }); + return; + } + + if (project.owner !== owner || project.owner !== owner || project.owner !== owner) { + res.log.warn('mismatch between project git details to payload git details'); + res.status(403).send({ error: 'forbidden: mismatch between project git details to payload git details' }); + return; + } + const result = await createOctokitClientByAction(git, res.log); + + if (!result.authenticated) { + res.status(403).send({ error: result.error }); + return; + } + + installationOctokit = result.installationOctokit; + } + + if (!installationOctokit) { + res.log.warn({ owner, repo, commitSha }, 'no git client'); + res.status(403).send({ error: 'forbidden' }); + return; + } + + return installationOctokit; +} diff --git a/service/src/controllers/legacyGithubController.ts b/service/src/controllers/legacyGithubController.ts index 96d04ae..d5f69c4 100644 --- a/service/src/controllers/legacyGithubController.ts +++ b/service/src/controllers/legacyGithubController.ts @@ -1,10 +1,6 @@ -import { - GithubOutputResponse, - GithubOutputTypes, - OutputResponse, - Status, - getReportConclusionText, -} from 'bundlemon-utils'; +// This file is deprecated + +import { Status, getReportConclusionText } from 'bundlemon-utils'; import { getInstallationId, createCheck, @@ -15,7 +11,7 @@ import { } from '../framework/github'; import { generateReportMarkdownWithLinks } from './utils/markdownReportGenerator'; import { checkAuth } from './utils/auth'; -import { promiseAllObject } from '../utils/promiseUtils'; +import { createGithubOutputs } from './utils/githubOutputs'; import type { FastifyValidatedRoute, @@ -206,12 +202,10 @@ export const legacyGithubOutputController: FastifyValidatedRoute>> = {}; - - if (output.checkRun) { - const summary = generateReportMarkdownWithLinks(report); - - tasks.checkRun = createCheck({ - subProject, - owner, - repo, - commitSha, - installationOctokit, - detailsUrl: report.metadata.linkToReport || undefined, - title: getReportConclusionText(report), - summary, - conclusion: report.status === Status.Pass ? 'success' : 'failure', - log: req.log, - }); - } - - if (output.commitStatus) { - tasks.commitStatus = createCommitStatus({ - subProject, - owner, - repo, - commitSha, - installationOctokit, - state: report.status === Status.Pass ? 'success' : 'error', - description: getReportConclusionText(report), - targetUrl: report.metadata.linkToReport || undefined, - log: req.log, - }); - } - - if (output.prComment) { - const title = subProject ? `BundleMon (${subProject})` : 'BundleMon'; - const body = `${genCommentIdentifier(subProject)}\n## ${title}\n${generateReportMarkdownWithLinks(report)}`; - - tasks.prComment = createOrUpdatePRComment({ - subProject, - owner, - repo, - prNumber, - installationOctokit, - body, - log: req.log, - }); - } - - const response: GithubOutputResponse = await promiseAllObject(tasks); + const response = await createGithubOutputs({ git, output, report, subProject, installationOctokit, log: req.log }); res.send(response); } catch (err) { diff --git a/service/src/controllers/projectsController.ts b/service/src/controllers/projectsController.ts index 9c87d6d..d5d2f98 100644 --- a/service/src/controllers/projectsController.ts +++ b/service/src/controllers/projectsController.ts @@ -25,8 +25,6 @@ export const getOrCreateProjectIdController: FastifyValidatedRoute { const { provider, owner, repo } = req.body; - // TODO: use checkAuth? - const id = await getOrCreateProjectId({ provider, owner: owner.toLowerCase(), repo: repo.toLowerCase() }); res.send({ id }); diff --git a/service/src/controllers/utils/auth.ts b/service/src/controllers/utils/auth.ts index b8209c8..58fc329 100644 --- a/service/src/controllers/utils/auth.ts +++ b/service/src/controllers/utils/auth.ts @@ -16,6 +16,12 @@ type CheckAuthResponse = } | { authenticated: true; installationOctokit?: Octokit }; +/** + * Verify the request is related to the project id, auth options: + * 1. API key - project is a simple project with API key. + * 2. GitHub actions - project is a git project, with GitHub provider. + * 3. GitHub actions - project is a simple project with API key - this will be removed soon. + */ export async function checkAuth( projectId: string, headers: AuthHeaders, @@ -31,7 +37,7 @@ export async function checkAuth( } if ('x-api-key' in headers) { - return handleProjectAuth(project, headers['x-api-key'], log); + return handleApiKeyAuth(project, headers['x-api-key'], log); } // deprecated @@ -48,7 +54,7 @@ export async function checkAuth( return { authenticated: false, error: 'forbidden' }; } -async function handleProjectAuth( +async function handleApiKeyAuth( project: Project, apiKey: string, log: FastifyLoggerInstance diff --git a/service/src/controllers/utils/githubOutputs.ts b/service/src/controllers/utils/githubOutputs.ts new file mode 100644 index 0000000..538e4d8 --- /dev/null +++ b/service/src/controllers/utils/githubOutputs.ts @@ -0,0 +1,89 @@ +import { + GithubOutputResponse, + GithubOutputTypes, + OutputResponse, + Report, + Status, + getReportConclusionText, +} from 'bundlemon-utils'; +import { createCheck, createCommitStatus, createOrUpdatePRComment, genCommentIdentifier } from '../../framework/github'; +import { generateReportMarkdownWithLinks } from './markdownReportGenerator'; +import { promiseAllObject } from '../../utils/promiseUtils'; + +import type { FastifyLoggerInstance } from 'fastify'; +import type { Octokit } from '@octokit/rest'; + +interface CreateGithubOutputParams { + git: { + owner: string; + repo: string; + commitSha: string; + prNumber?: string; + }; + output: Partial>; + report: Report; + subProject?: string; + installationOctokit: Octokit; + log: FastifyLoggerInstance; +} + +export async function createGithubOutputs({ + git: { owner, repo, commitSha, prNumber }, + output, + report, + subProject, + installationOctokit, + log, +}: CreateGithubOutputParams) { + const tasks: Partial>> = {}; + + if (output.checkRun) { + const summary = generateReportMarkdownWithLinks(report); + + tasks.checkRun = createCheck({ + subProject, + owner, + repo, + commitSha, + installationOctokit, + detailsUrl: report.metadata.linkToReport || undefined, + title: getReportConclusionText(report), + summary, + conclusion: report.status === Status.Pass ? 'success' : 'failure', + log: log, + }); + } + + if (output.commitStatus) { + tasks.commitStatus = createCommitStatus({ + subProject, + owner, + repo, + commitSha, + installationOctokit, + state: report.status === Status.Pass ? 'success' : 'error', + description: getReportConclusionText(report), + targetUrl: report.metadata.linkToReport || undefined, + log: log, + }); + } + + if (output.prComment) { + const title = subProject ? `BundleMon (${subProject})` : 'BundleMon'; + const body = `${genCommentIdentifier(subProject)}\n## ${title}\n${generateReportMarkdownWithLinks(report)}`; + + tasks.prComment = createOrUpdatePRComment({ + subProject, + owner, + repo, + prNumber, + installationOctokit, + body, + log: log, + }); + } + + const response: GithubOutputResponse = await promiseAllObject(tasks); + + return response; +} diff --git a/service/src/routes/api/v1.ts b/service/src/routes/api/v1.ts index 6e1de21..c2a276a 100644 --- a/service/src/routes/api/v1.ts +++ b/service/src/routes/api/v1.ts @@ -14,9 +14,8 @@ import { LegacyGithubOutputRequestSchema, GetSubprojectsRequestSchema, GetOrCreateProjectIdRequestSchema, + GithubOutputRequestSchema, } from '../../consts/schemas'; - -import type { FastifyPluginCallback } from 'fastify'; import { createGithubCheckController, createGithubCommitStatusController, @@ -24,10 +23,15 @@ import { legacyGithubOutputController, } from '../../controllers/legacyGithubController'; import { getSubprojectsController } from '../../controllers/subprojectsController'; +import { githubOutputController } from '../..//controllers/githubController'; + +import type { FastifyPluginCallback } from 'fastify'; const commitRecordRoutes: FastifyPluginCallback = (app, _opts, done) => { app.get('/base', { schema: GetCommitRecordRequestSchema.properties }, getCommitRecordWithBaseController); + app.post('/outputs/github', { schema: GithubOutputRequestSchema.properties }, githubOutputController); + done(); }; @@ -40,6 +44,7 @@ const commitRecordsRoutes: FastifyPluginCallback = (app, _opts, done) => { done(); }; +// @deprecated const outputsRoutes: FastifyPluginCallback = (app, _opts, done) => { // bundlemon > v0.4.0 app.post('/github', { schema: LegacyGithubOutputRequestSchema.properties }, legacyGithubOutputController); From f5f4c6305902f630ec7831fefc6318e6d95c7ea0 Mon Sep 17 00:00:00 2001 From: LironEr Date: Fri, 3 Jun 2022 15:43:03 +0300 Subject: [PATCH 4/6] Add funding & badges --- .github/FUNDING.yml | 13 +++++++++++++ README.md | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..13fc574 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: lironer +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: ['https://www.paypal.com/donate?hosted_button_id=YZL6KM2UGB5ML'] \ No newline at end of file diff --git a/README.md b/README.md index f985bc6..8c91347 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![npm](https://img.shields.io/npm/v/bundlemon)](http://www.npmjs.com/package/bundlemon) [![node](https://img.shields.io/node/v/bundlemon.svg)](https://github.com/LironEr/bundlemon) +[![npm](https://img.shields.io/npm/l/bundlemon)](http://www.npmjs.com/package/bundlemon) +[![npm](https://img.shields.io/npm/dm/bundlemon)](http://www.npmjs.com/package/bundlemon) BundleMon helps you to monitor your bundle size. From e6896f325114e6c497161301183c5b8c8cfdbb5a Mon Sep 17 00:00:00 2001 From: LironEr Date: Fri, 3 Jun 2022 17:08:24 +0300 Subject: [PATCH 5/6] bump utils version & fix tests --- packages/bundlemon-utils/package.json | 2 +- .../src/__tests__/__snapshots__/consts.spec.ts.snap | 3 +++ service/scripts/generateLocalData.ts | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/bundlemon-utils/package.json b/packages/bundlemon-utils/package.json index 812af28..993402d 100644 --- a/packages/bundlemon-utils/package.json +++ b/packages/bundlemon-utils/package.json @@ -1,6 +1,6 @@ { "name": "bundlemon-utils", - "version": "0.4.0", + "version": "0.4.1", "description": "", "keywords": [], "author": "Liron Er", diff --git a/packages/bundlemon-utils/src/__tests__/__snapshots__/consts.spec.ts.snap b/packages/bundlemon-utils/src/__tests__/__snapshots__/consts.spec.ts.snap index df80883..2ca052d 100644 --- a/packages/bundlemon-utils/src/__tests__/__snapshots__/consts.spec.ts.snap +++ b/packages/bundlemon-utils/src/__tests__/__snapshots__/consts.spec.ts.snap @@ -17,6 +17,9 @@ Object { "MaxPercentIncrease": "MaxPercentIncrease", "MaxSize": "MaxSize", }, + "ProjectProvider": Object { + "GitHub": "github", + }, "Status": Object { "Fail": "Fail", "Pass": "Pass", diff --git a/service/scripts/generateLocalData.ts b/service/scripts/generateLocalData.ts index 8fe970e..3e9234e 100644 --- a/service/scripts/generateLocalData.ts +++ b/service/scripts/generateLocalData.ts @@ -3,7 +3,8 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: path.join(__dirname, '../dev.env') }); import { ObjectId } from 'mongodb'; -import { getDB, getProjectsCollection } from '../src/framework/mongo'; +import { getDB } from '../src/framework/mongo/client'; +import { getProjectsCollection } from '../src/framework/mongo/projects'; import { createHash } from '../src/utils/hashUtils'; import { mongoUrl, nodeEnv } from '../src/framework/env'; @@ -20,6 +21,7 @@ async function createProject(char: string) { _id: new ObjectId(projectId), apiKey: { hash, startKey }, creationDate, + lastAccessed: new Date(), }); console.log(`project ${projectId} created with api key: "${apiKey}"`); From 1b03a5e89dbf843b7ccea9fb813617ce3aced61a Mon Sep 17 00:00:00 2001 From: LironEr Date: Sat, 4 Jun 2022 10:19:27 +0300 Subject: [PATCH 6/6] update utils version --- packages/bundlemon-markdown-output/package.json | 2 +- packages/bundlemon/package.json | 2 +- service/package.json | 2 +- website/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bundlemon-markdown-output/package.json b/packages/bundlemon-markdown-output/package.json index cd672f3..28d53b0 100644 --- a/packages/bundlemon-markdown-output/package.json +++ b/packages/bundlemon-markdown-output/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "bytes": "^3.1.0", - "bundlemon-utils": "^0.4.0" + "bundlemon-utils": "^0.4.1" }, "devDependencies": { "@types/bytes": "^3.1.0", diff --git a/packages/bundlemon/package.json b/packages/bundlemon/package.json index 5e92d26..1cbba00 100644 --- a/packages/bundlemon/package.json +++ b/packages/bundlemon/package.json @@ -36,7 +36,7 @@ "dependencies": { "axios": "^0.21.1", "brotli-size": "^4.0.0", - "bundlemon-utils": "^0.4.0", + "bundlemon-utils": "^0.4.1", "bytes": "^3.1.0", "chalk": "^4.1.1", "commander": "^8.0.0", diff --git a/service/package.json b/service/package.json index ae36d73..53a4a5d 100644 --- a/service/package.json +++ b/service/package.json @@ -24,7 +24,7 @@ "@octokit/auth-app": "^3.4.0", "@octokit/rest": "^18.5.3", "aws-lambda-fastify": "^1.4.4", - "bundlemon-utils": "^0.4.0", + "bundlemon-utils": "^0.4.1", "bundlemon-markdown-output": "^0.1.1", "bytes": "^3.1.0", "env-var": "^7.0.1", diff --git a/website/package.json b/website/package.json index 39154ae..df0ff06 100644 --- a/website/package.json +++ b/website/package.json @@ -16,7 +16,7 @@ "@emotion/styled": "^11.3.0", "@mui/lab": "^5.0.0-alpha.61", "@mui/material": "^5.2.5", - "bundlemon-utils": "^0.4.0", + "bundlemon-utils": "^0.4.1", "mobx": "^6.3.5", "mobx-react-lite": "^3.2.1", "react": "^17.0.2",