diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index a11a25fe6a6..54fa8550a8a 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -246,7 +246,7 @@ "summary": "Build docker with platform", "description": "use to build all docker containers required for platform", "safeForSimultaneousRushProcesses": true, - "shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool --to @hcengineering/pod-stats --to @hcengineering/pod-fulltext --to @hcengineering/pod-love --to @hcengineering/green --to @hcengineering/pod-mail --to @hcengineering/pod-datalake" + "shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool --to @hcengineering/pod-stats --to @hcengineering/pod-fulltext --to @hcengineering/pod-love --to @hcengineering/green --to @hcengineering/pod-mail --to @hcengineering/pod-datalake --to @hcengineering/pod-hook" }, { "commandKind": "global", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 9dba2555d0d..a1045ebeb5c 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -703,6 +703,9 @@ importers: '@rush-temp/pod-gmail': specifier: file:./projects/pod-gmail.tgz version: file:projects/pod-gmail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)) + '@rush-temp/pod-hook': + specifier: file:./projects/pod-hook.tgz + version: file:projects/pod-hook.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9)) '@rush-temp/pod-love': specifier: file:./projects/pod-love.tgz version: file:projects/pod-love.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4) @@ -4738,7 +4741,7 @@ packages: version: 0.0.0 '@rush-temp/my-space@file:projects/my-space.tgz': - resolution: {integrity: sha512-lZXMkLZnlmpSkjEj4lV2pxIaObjgAQ3OT4ktzUOzlpP1v3U78XKLJzRQ29SqVBYH+DcEHqL97znDPEGxzYHZKA==, tarball: file:projects/my-space.tgz} + resolution: {integrity: sha512-HyPJ+wTQXLVKvzmJM7NzfJsEAYgBy6lszwGwdN2MLGIFPTT79621OOxX2Z7pEb120/G9r+BJuriVzbmJEZqRZA==, tarball: file:projects/my-space.tgz} version: 0.0.0 '@rush-temp/notification-assets@file:projects/notification-assets.tgz': @@ -4825,6 +4828,10 @@ packages: resolution: {integrity: sha512-aYecRf97Yj23bi2BdWXg4DrH4oy3AB2UfrpbwNo6nvLhHJLsrcRgOe9zCTakZ7Yb+nDzsxYuOwZiDIr9aWtsyw==, tarball: file:projects/pod-gmail.tgz} version: 0.0.0 + '@rush-temp/pod-hook@file:projects/pod-hook.tgz': + resolution: {integrity: sha512-RgcAHfNOUxdFKPBrtcgIsNpQLo4ro8kuPIbNvG8omy/njf/HHWU5YJMFlznSlv+gCrvZmGsP9YV4k+X0uL/bdw==, tarball: file:projects/pod-hook.tgz} + version: 0.0.0 + '@rush-temp/pod-love@file:projects/pod-love.tgz': resolution: {integrity: sha512-m+9dPMR45FJpQB+M5ka4PiJcTpV8COlQA5F36is2LkMLwnsDjcB94Sx+TFdIyjhVFpfB8l+t886mSPveHql4ag==, tarball: file:projects/pod-love.tgz} version: 0.0.0 @@ -21518,6 +21525,41 @@ snapshots: - supports-color - ts-node + '@rush-temp/pod-hook@file:projects/pod-hook.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))': + dependencies: + '@tsconfig/node16': 1.0.4 + '@types/cors': 2.8.17 + '@types/express': 4.17.21 + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3) + cors: 2.8.5 + cross-env: 7.0.3 + dotenv: 16.0.3 + esbuild: 0.24.2 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + express: 4.21.2 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)) + prettier: 3.2.5 + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.7.3) + ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - node-notifier + - supports-color + '@rush-temp/pod-love@file:projects/pod-love.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@tsconfig/node16': 1.0.4 diff --git a/rush.json b/rush.json index 44df1d2a597..8e0b4511878 100644 --- a/rush.json +++ b/rush.json @@ -2362,6 +2362,11 @@ "packageName": "@hcengineering/pod-mail", "projectFolder": "services/mail/pod-mail", "shouldPublish": false + }, + { + "packageName": "@hcengineering/pod-hook", + "projectFolder": "services/hook/pod-hook", + "shouldPublish": false } ] } diff --git a/services/hook/pod-hook/.eslintrc.js b/services/hook/pod-hook/.eslintrc.js new file mode 100644 index 00000000000..72235dc2833 --- /dev/null +++ b/services/hook/pod-hook/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/services/hook/pod-hook/.gitignore b/services/hook/pod-hook/.gitignore new file mode 100644 index 00000000000..2eea525d885 --- /dev/null +++ b/services/hook/pod-hook/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/services/hook/pod-hook/.npmignore b/services/hook/pod-hook/.npmignore new file mode 100644 index 00000000000..e3ec093c383 --- /dev/null +++ b/services/hook/pod-hook/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/services/hook/pod-hook/Dockerfile b/services/hook/pod-hook/Dockerfile new file mode 100644 index 00000000000..33ebc8e0b03 --- /dev/null +++ b/services/hook/pod-hook/Dockerfile @@ -0,0 +1,7 @@ +FROM hardcoreeng/base:v20250310 +WORKDIR /usr/src/app + +COPY bundle/bundle.js ./ + +EXPOSE 8098 +CMD [ "node", "bundle.js" ] diff --git a/services/hook/pod-hook/build.sh b/services/hook/pod-hook/build.sh new file mode 100755 index 00000000000..d52b6258ea5 --- /dev/null +++ b/services/hook/pod-hook/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright © 2025 Hardcore Engineering Inc. +# +# Licensed under the Eclipse Public License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# See the License for the specific language governing permissions and +# limitations under the License. +# + +rushx bundle +rushx docker:build +rushx docker:push diff --git a/services/hook/pod-hook/config/rig.json b/services/hook/pod-hook/config/rig.json new file mode 100644 index 00000000000..0110930f55e --- /dev/null +++ b/services/hook/pod-hook/config/rig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig" +} diff --git a/services/hook/pod-hook/jest.config.js b/services/hook/pod-hook/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/services/hook/pod-hook/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/services/hook/pod-hook/package.json b/services/hook/pod-hook/package.json new file mode 100644 index 00000000000..a52e5fcc107 --- /dev/null +++ b/services/hook/pod-hook/package.json @@ -0,0 +1,60 @@ +{ + "name": "@hcengineering/pod-hook", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "_phase:bundle": "rushx bundle", + "_phase:docker-build": "rushx docker:build", + "_phase:docker-staging": "rushx docker:staging", + "bundle": "node ../../../common/scripts/esbuild.js", + "docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/hook", + "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/hook staging", + "docker:abuild": "docker build -t hardcoreeng/hook . --platform=linux/arm64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/hook", + "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/hook", + "run-local": "ts-node src/index.ts", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "cross-env": "~7.0.3", + "@hcengineering/platform-rig": "^0.6.0", + "@types/node": "~20.11.16", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "esbuild": "^0.24.2", + "prettier": "^3.1.0", + "ts-node": "^10.8.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@tsconfig/node16": "^1.0.4", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "eslint-plugin-node": "^11.1.0" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2", + "dotenv": "~16.0.0" + } +} diff --git a/services/hook/pod-hook/src/config.ts b/services/hook/pod-hook/src/config.ts new file mode 100644 index 00000000000..170c5246937 --- /dev/null +++ b/services/hook/pod-hook/src/config.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { config as dotenvConfig } from 'dotenv' + +dotenvConfig() + +export interface Config { + port: number +} + +const envMap = { + Port: 'PORT' +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const port = parseNumber(process.env[envMap.Port]) + if (port === undefined) { + throw Error('Missing env variable: Port') + } + const params: Config = { + port + } + + return params +})() + +export default config diff --git a/services/hook/pod-hook/src/error.ts b/services/hook/pod-hook/src/error.ts new file mode 100644 index 00000000000..98eb3adba69 --- /dev/null +++ b/services/hook/pod-hook/src/error.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} diff --git a/services/hook/pod-hook/src/index.ts b/services/hook/pod-hook/src/index.ts new file mode 100644 index 00000000000..a2ce3d2a244 --- /dev/null +++ b/services/hook/pod-hook/src/index.ts @@ -0,0 +1,22 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { main } from './main' + +void main().catch((err) => { + if (err != null) { + console.error(err) + } +}) diff --git a/services/hook/pod-hook/src/main.ts b/services/hook/pod-hook/src/main.ts new file mode 100644 index 00000000000..dd56f12f410 --- /dev/null +++ b/services/hook/pod-hook/src/main.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { createServer, listen } from './server' +import { Endpoint } from './types' +import config from './config' + +export const main = async (): Promise => { + const endpoints: Endpoint[] = [ + { + endpoint: '/mta', + type: 'post', + handler: async (req, res) => { + console.log('mta-hook retrieved') + const message = getMessageInfo(req.body) + console.log('Email from:', message?.from) + // TODO: Send request to add message or put event to the queue + + res.json({ + action: 'accept' + }) + } + } + ] + + const server = listen(createServer(endpoints), config.port) + + const shutdown = (): void => { + server.close(() => { + process.exit() + }) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} + +const getMessageInfo = ( + body: any +): { from: string, to: string[], subject: string, contents: any, size: number } | undefined => { + try { + const from = body.envelope.from.address + const to = body.envelope.to.map((recipient: any) => recipient.address) + const subjectHeader = body.message.headers.find((header: any) => header[0] === 'Subject') + const subject = subjectHeader !== undefined ? subjectHeader[1] : 'No Subject' + const contents = body.message.contents + const size = body.message.size + + return { + from, + to, + subject, + contents, + size + } + } catch (e) { + console.error('Failed to parse message:', e) + return undefined + } +} diff --git a/services/hook/pod-hook/src/server.ts b/services/hook/pod-hook/src/server.ts new file mode 100644 index 00000000000..8d44919695e --- /dev/null +++ b/services/hook/pod-hook/src/server.ts @@ -0,0 +1,69 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import cors from 'cors' +import express, { Express, NextFunction, Request, Response } from 'express' +import { Server } from 'http' + +import { Endpoint, RequestHandler } from './types' +import { ApiError } from './error' + +const catchError = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => { + void (async () => { + try { + await fn(req, res, next) + } catch (err: unknown) { + next(err) + } + })() +} + +export function createServer (endpoints: Endpoint[]): Express { + const app = express() + + app.use(cors()) + app.use(express.json()) + + endpoints.forEach((endpoint) => { + if (endpoint.type === 'get') { + app.get(endpoint.endpoint, catchError(endpoint.handler)) + } else if (endpoint.type === 'post') { + app.post(endpoint.endpoint, catchError(endpoint.handler)) + } + }) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + app.use((err: any, _req: any, res: any, _next: any) => { + if (err instanceof ApiError) { + res.status(400).send({ code: err.code, message: err.message }) + return + } + + res.status(500).send({ message: err.message }) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Hook service has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/services/hook/pod-hook/src/types.ts b/services/hook/pod-hook/src/types.ts new file mode 100644 index 00000000000..e47be22f1a8 --- /dev/null +++ b/services/hook/pod-hook/src/types.ts @@ -0,0 +1,26 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { NextFunction, Request, Response } from 'express' + +export type RequestType = 'get' | 'post' + +export type RequestHandler = (req: Request, res: Response, next?: NextFunction) => Promise + +export interface Endpoint { + endpoint: string + type: RequestType + handler: RequestHandler +} diff --git a/services/hook/pod-hook/tsconfig.json b/services/hook/pod-hook/tsconfig.json new file mode 100644 index 00000000000..59e4fd42978 --- /dev/null +++ b/services/hook/pod-hook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file