Chore: Move micro services to packages (#26884)

This commit is contained in:
Diego Sampaio 2022-09-22 23:43:57 -03:00 committed by GitHub
parent 9625a994b8
commit 4ab9c35f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1102 additions and 475 deletions

View File

@ -156,9 +156,8 @@ jobs:
- name: Reset Meteor
if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop'
run: |
cd ./apps/meteor
meteor reset
working-directory: ./apps/meteor
run: meteor reset
- name: Build Rocket.Chat From Pull Request
if: startsWith(github.ref, 'refs/pull/') == true
@ -232,6 +231,29 @@ jobs:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs0
- name: Docker env vars
id: docker-env
run: |
LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}"
echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}"
# test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once)
if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then
RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine"
RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine"
else
RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile"
RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official"
fi;
echo "RC_DOCKERFILE: ${RC_DOCKERFILE}"
echo "::set-output name=rc-dockerfile::${RC_DOCKERFILE}"
echo "RC_DOCKER_TAG: ${RC_DOCKER_TAG}"
echo "::set-output name=rc-docker-tag::${RC_DOCKER_TAG}"
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
@ -264,29 +286,6 @@ jobs:
tar xzf Rocket.Chat.tar.gz
rm Rocket.Chat.tar.gz
- name: Docker env vars
id: docker-env
run: |
LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}"
echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}"
# test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once)
if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then
RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine"
RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine"
else
RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile"
RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official"
fi;
echo "RC_DOCKERFILE: ${RC_DOCKERFILE}"
echo "::set-output name=rc-dockerfile::${RC_DOCKERFILE}"
echo "RC_DOCKER_TAG: ${RC_DOCKER_TAG}"
echo "::set-output name=rc-docker-tag::${RC_DOCKER_TAG}"
- name: Start containers
env:
MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true'
@ -298,14 +297,6 @@ jobs:
run: |
docker compose -f docker-compose-ci.yml up -d --build rocketchat
sleep 10
until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1
sleep 10
done
- name: Login to GitHub Container Registry
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop'
uses: docker/login-action@v2
@ -333,7 +324,7 @@ jobs:
docker push $IMAGE_NAME_BASE
fi;
- name: E2E Test API
- name: Wait for Rocket.Chat to start up
env:
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }}
@ -341,9 +332,21 @@ jobs:
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
run: |
docker ps
docker compose -f docker-compose-ci.yml logs rocketchat --tail=50
cd ./apps/meteor
until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1
sleep 10
done;
- name: E2E Test API
env:
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }}
RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }}
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
working-directory: ./apps/meteor
run: |
for i in $(seq 1 5); do
npm run testapi && s=0 && break || s=$?
@ -376,18 +379,18 @@ jobs:
- name: Install Playwright
if: steps.cache-playwright.outputs.cache-hit != 'true'
run: |
cd ./apps/meteor
npx playwright install --with-deps
working-directory: ./apps/meteor
run: npx playwright install --with-deps
- name: E2E Test UI
- name: Reset containers
env:
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }}
RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }}
run: |
docker ps
docker compose -f docker-compose-ci.yml logs rocketchat --tail=50
docker compose -f docker-compose-ci.yml stop rocketchat
docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()'
@ -399,10 +402,11 @@ jobs:
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && exit 1
sleep 10
done
done;
cd ./apps/meteor
yarn test:e2e
- name: E2E Test UI
working-directory: ./apps/meteor
run: yarn test:e2e
- name: Store playwright test trace
uses: actions/upload-artifact@v2
@ -444,6 +448,9 @@ jobs:
- name: yarn install
run: yarn
- name: yarn build
run: yarn build
- name: Unit Test
run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc'
@ -459,33 +466,26 @@ jobs:
tar xzf Rocket.Chat.tar.gz
rm Rocket.Chat.tar.gz
- name: Docker env vars
id: docker-env
run: |
LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}"
echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}"
- name: Start containers
env:
MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true'
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile'
RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official'
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
TRANSPORTER: nats://nats:4222
ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }}
run: |
export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
docker compose -f docker-compose-ci.yml up -d --build
sleep 10
until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do
echo "Waiting 'ddp-streamer' to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1
sleep 10
done
until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1
sleep 10
done
- name: Login to GitHub Container Registry
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop'
uses: docker/login-action@v2
@ -498,9 +498,8 @@ jobs:
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop'
env:
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
run: |
export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]")
docker compose -f docker-compose-ci.yml push \
authorization-service \
account-service \
@ -508,14 +507,37 @@ jobs:
presence-service \
stream-hub-service
- name: E2E Test API
- name: Wait services to start up
env:
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile'
RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official'
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
run: |
docker ps
docker compose -f docker-compose-ci.yml logs --tail=50
cd ./apps/meteor
until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do
echo "Waiting 'ddp-streamer' to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1
sleep 10
done;
until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1
sleep 10
done;
- name: E2E Test API
env:
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile'
RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official'
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
working-directory: ./apps/meteor
run: |
for i in $(seq 1 5); do
IS_EE=true npm run testapi && s=0 && break || s=$?;
IS_EE=true npm run testapi && s=0 && break || s=$?
docker compose -f ../../docker-compose-ci.yml logs --tail=100
@ -525,7 +547,7 @@ jobs:
NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z")
docker compose -f ../../docker-compose-ci.yml start
docker compose -f ../../docker-compose-ci.yml restart
until echo "$(docker compose -f ../../docker-compose-ci.yml logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
@ -535,6 +557,33 @@ jobs:
done;
exit $s
- name: Reset containers
env:
LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }}
RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile'
RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official'
DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}
run: |
docker compose -f docker-compose-ci.yml stop
docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()'
NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z")
docker compose -f docker-compose-ci.yml restart
until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do
echo "Waiting 'ddp-streamer' to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1
sleep 10
done;
until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1
sleep 10
done;
- name: Cache Playwright binaries
uses: actions/cache@v3
id: cache-playwright
@ -545,30 +594,12 @@ jobs:
key: playwright-1.23.1
- name: Install Playwright
run: |
cd ./apps/meteor
npx playwright install --with-deps
working-directory: ./apps/meteor
run: npx playwright install --with-deps
- name: E2E Test UI
run: |
docker ps
docker compose -f docker-compose-ci.yml logs rocketchat --tail=50
docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()'
NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z")
docker compose -f docker-compose-ci.yml restart
until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do
echo "Waiting Rocket.Chat to start up"
((c++)) && ((c==10)) && exit 1
sleep 10
done
cd ./apps/meteor
E2E_COVERAGE=true IS_EE=true yarn test:e2e
working-directory: ./apps/meteor
run: E2E_COVERAGE=true IS_EE=true yarn test:e2e
- name: Store playwright test trace
uses: actions/upload-artifact@v2
@ -578,9 +609,8 @@ jobs:
path: ./apps/meteor/tests/e2e/.playwright*
- name: Extract e2e:ee:coverage
run: |
cd ./apps/meteor
yarn test:e2e:nyc
working-directory: ./apps/meteor
run: yarn test:e2e:nyc
- uses: codecov/codecov-action@v3
with:
@ -597,7 +627,7 @@ jobs:
deploy:
runs-on: ubuntu-20.04
if: github.event_name == 'release' || github.ref == 'refs/heads/develop'
needs: [test, release-versions]
needs: [test, test-ee, release-versions]
steps:
- uses: actions/checkout@v3

View File

@ -36,6 +36,7 @@ import { getServicesStatistics } from './getServicesStatistics';
import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server';
import { Analytics, Team, VideoConf } from '../../../../server/sdk';
import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics';
import { isRunningMs } from '../../../../server/lib/isRunningMs';
const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server'];
@ -342,6 +343,7 @@ export const statistics = {
);
const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo();
statistics.msEnabled = isRunningMs();
statistics.oplogEnabled = oplogEnabled;
statistics.mongoVersion = mongoVersion;
statistics.mongoStorageEngine = mongoStorageEngine;

View File

@ -125,6 +125,7 @@ export default {
lockedAt: '',
},
instanceCount: 0,
msEnabled: false,
oplogEnabled: false,
mongoVersion: '',
mongoStorageEngine: '',

View File

@ -56,9 +56,9 @@ const DeploymentCard = ({ info, statistics, instances }: DeploymentCardProps): R
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('MongoDB')}</Card.Col.Title>
{`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} (oplog ${
statistics.oplogEnabled ? t('Enabled') : t('Disabled')
})`}
{`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} ${
!statistics.msEnabled ? `(oplog ${statistics.oplogEnabled ? t('Enabled') : t('Disabled')})` : ''
}`}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Commit_details')}</Card.Col.Title>

View File

@ -155,6 +155,7 @@ export default {
lockedAt: '',
},
instanceCount: 0,
msEnabled: false,
oplogEnabled: false,
mongoVersion: '',
mongoStorageEngine: '',

View File

@ -33,8 +33,8 @@ const InformationPage = memo(function InformationPage({
return null;
}
const usingMultipleInstances = statistics?.instanceCount > 1;
const alertOplogForMultipleInstances = usingMultipleInstances && !statistics.oplogEnabled;
const warningMultipleInstances = !statistics?.msEnabled && statistics?.instanceCount > 1;
const alertOplogForMultipleInstances = warningMultipleInstances && !statistics.oplogEnabled;
return (
<Page data-qa='admin-info'>
@ -53,7 +53,9 @@ const InformationPage = memo(function InformationPage({
<Page.ScrollableContentWithShadow>
<Box marginBlock='none' marginInline='auto' width='full'>
{usingMultipleInstances && <Callout type='danger' title={t('Multiple_monolith_instances_alert')} marginBlockEnd='x16'></Callout>}
{warningMultipleInstances && (
<Callout type='danger' title={t('Multiple_monolith_instances_alert')} marginBlockEnd='x16'></Callout>
)}
{alertOplogForMultipleInstances && (
<Callout
type='danger'

View File

@ -103,6 +103,7 @@ export default {
lockedAt: '',
},
instanceCount: 0,
msEnabled: false,
oplogEnabled: false,
mongoVersion: '',
mongoStorageEngine: '',

View File

@ -34,9 +34,6 @@ export class NetworkBroker implements IBroker {
this.broker = broker;
this.metrics = broker.metrics;
// TODO move this to a proper startup method?
this.started = this.broker.start();
}
async call(method: string, data: any): Promise<any> {
@ -85,11 +82,6 @@ export class NetworkBroker implements IBroker {
: Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
).filter((name) => name !== 'constructor');
const instanceEvents = instance.getEvents();
if (!instanceEvents && !methods.length) {
return;
}
const serviceInstance = instance as any;
const name = instance.getName();
@ -97,7 +89,7 @@ export class NetworkBroker implements IBroker {
if (!instance.isInternal()) {
instance.onEvent('shutdown', async (services) => {
if (!services[name]?.includes(this.broker.nodeID)) {
this.broker.logger.debug({ msg: 'Not shutting down, different node.', nodeID: this.broker.nodeID });
this.broker.logger.info({ msg: 'Not shutting down, different node.', nodeID: this.broker.nodeID });
return;
}
this.broker.logger.warn({ msg: 'Received shutdown event, destroying service.', nodeID: this.broker.nodeID });
@ -105,6 +97,11 @@ export class NetworkBroker implements IBroker {
});
}
const instanceEvents = instance.getEvents();
if (!instanceEvents && !methods.length) {
return;
}
const dependencies = name !== 'license' ? { dependencies: ['license'] } : {};
const service: ServiceSchema = {
@ -185,4 +182,8 @@ export class NetworkBroker implements IBroker {
async nodeList(): Promise<IBrokerNode[]> {
return this.broker.call('$node.list');
}
async start(): Promise<void> {
this.started = this.broker.start();
}
}

View File

@ -1,147 +0,0 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { IHashedStampedToken } from './lib/utils';
import { _generateStampedLoginToken, _hashStampedToken, _hashLoginToken, _tokenExpiration, validatePassword } from './lib/utils';
import { getCollection, Collections } from '../mongo';
import { ServiceClass } from '../../../../server/sdk/types/ServiceClass';
import type { IAccount, ILoginResult } from '../../../../server/sdk/types/IAccount';
import { MeteorError } from '../../../../server/sdk/errors';
const saveSession = async (uid: string, newToken: IHashedStampedToken): Promise<void> => {
const Users = await getCollection<IUser>(Collections.User);
await Users.updateOne(
{ _id: uid },
{
$push: {
'services.resume.loginTokens': newToken.hashedToken,
},
},
);
};
const removeSession = async (uid: string, loginToken: string): Promise<void> => {
const Users = await getCollection<IUser>(Collections.User);
await Users.updateOne(
{ _id: uid },
{
$pull: {
'services.resume.loginTokens': {
$or: [{ hashedToken: loginToken }, { token: loginToken }],
},
},
},
);
};
const loginViaResume = async (resume: string, loginExpiration: number): Promise<false | ILoginResult> => {
const Users = await getCollection<IUser>(Collections.User);
const hashedToken = _hashLoginToken(resume);
const user = await Users.findOne<IUser>(
{
'services.resume.loginTokens.hashedToken': hashedToken,
},
{
projection: {
'services.resume.loginTokens': 1,
},
},
);
if (!user) {
return false;
}
const { when } = user.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken) || {};
const tokenExpires = when && _tokenExpiration(when, loginExpiration);
if (tokenExpires && new Date() >= tokenExpires) {
throw new MeteorError(403, 'Your session has expired. Please log in again.');
}
return {
uid: user._id,
token: resume,
hashedToken,
type: 'resume',
...(tokenExpires && { tokenExpires }),
};
};
const loginViaUsername = async (
{ username }: { username: string },
password: string,
loginExpiration: number,
): Promise<false | ILoginResult> => {
const Users = await getCollection<IUser>(Collections.User);
const user = await Users.findOne<IUser>({ username }, { projection: { 'services.password.bcrypt': 1 } });
if (!user) {
return false;
}
const valid = user.services?.password?.bcrypt && validatePassword(password, user.services.password.bcrypt);
if (!valid) {
return false;
}
const newToken = _generateStampedLoginToken();
const hashedToken = _hashStampedToken(newToken);
await saveSession(user._id, hashedToken);
return {
uid: user._id,
token: newToken.token,
hashedToken: hashedToken.hashedToken,
tokenExpires: _tokenExpiration(newToken.when, loginExpiration),
type: 'password',
};
};
export class Account extends ServiceClass implements IAccount {
protected name = 'accounts';
private loginExpiration = 90;
constructor() {
super();
this.onEvent('watch.settings', async ({ clientAction, setting }): Promise<void> => {
if (clientAction === 'removed') {
return;
}
const { _id, value } = setting;
if (_id !== 'Accounts_LoginExpiration') {
return;
}
if (typeof value === 'number') {
this.loginExpiration = value;
}
});
}
async login({ resume, user, password }: { resume: string; user: { username: string }; password: string }): Promise<false | ILoginResult> {
if (resume) {
return loginViaResume(resume, this.loginExpiration);
}
if (user && password) {
return loginViaUsername(user, password, this.loginExpiration);
}
return false;
}
async logout({ userId, token }: { userId: string; token: string }): Promise<void> {
return removeSession(userId, token);
}
async started(): Promise<void> {
const Settings = await getCollection<any>(Collections.Settings);
const expiry = await Settings.findOne({ _id: 'Accounts_LoginExpiration' }, { projection: { value: 1 } });
if (expiry?.value) {
this.loginExpiration = expiry.value;
}
}
}

View File

@ -1,6 +0,0 @@
import '../../startup/broker';
import { api } from '../../../../server/sdk/api';
import { Account } from './Account';
api.registerService(new Account());

View File

@ -1,16 +0,0 @@
import type { Document } from 'mongodb';
import '../../startup/broker';
import { api } from '../../../../server/sdk/api';
import { Authorization } from '../../../../server/services/authorization/service';
import { Collections, getCollection, getConnection } from '../mongo';
import { registerServiceModels } from '../../lib/registerServiceModels';
getConnection().then(async (db) => {
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
api.registerService(new Authorization(db));
});

View File

@ -1,33 +0,0 @@
const watch = ['.', '../broker.ts', '../../../server/sdk'];
module.exports = {
apps: [
{
name: 'authorization',
watch: [...watch, '../../../server/services/authorization'],
},
// {
// name: 'presence',
// },
{
name: 'account',
},
{
name: 'stream-hub',
},
// {
// name: 'ddp-streamer',
// },
].map((app) =>
Object.assign(app, {
script: app.script || `ts-node --files ${app.name}/service.ts`,
watch: app.watch || ['.', '../broker.ts', '../../../server/sdk', '../../../server/modules'],
instances: 1,
env: {
MOLECULER_LOG_LEVEL: 'info',
TRANSPORTER: 'nats://localhost:4222',
MONGO_URL: 'mongodb://localhost:3001/meteor',
},
}),
),
};

View File

@ -7,10 +7,6 @@
"scripts": {
"dev": "pm2 start ecosystem.config.js",
"pm2": "pm2",
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} run-p start:account start:authorization start:stream-hub",
"start:account": "ts-node --files ./account/service.ts",
"start:authorization": "ts-node --files ./authorization/service.ts",
"start:stream-hub": "ts-node --files ./stream-hub/service.ts",
"typecheck": "tsc --noEmit --skipLibCheck",
"build": "tsc",
"build-containers": "npm run build && docker-compose build && rm -rf ./dist",

View File

@ -1,20 +0,0 @@
import type { IServiceClass } from '../../../../server/sdk/types/ServiceClass';
import { ServiceClass } from '../../../../server/sdk/types/ServiceClass';
import { getConnection } from '../mongo';
import { initWatchers } from '../../../../server/modules/watchers/watchers.module';
import { api } from '../../../../server/sdk/api';
import { DatabaseWatcher } from '../../../../server/database/DatabaseWatcher';
export class StreamHub extends ServiceClass implements IServiceClass {
protected name = 'hub';
async created(): Promise<void> {
const db = await getConnection({ maxPoolSize: 1 });
const watcher = new DatabaseWatcher({ db });
initWatchers(watcher, api.broadcast.bind(api));
watcher.watch();
}
}

View File

@ -1,18 +0,0 @@
import type { Document } from 'mongodb';
import '../../startup/broker';
import { api } from '../../../../server/sdk/api';
import { Collections, getCollection, getConnection } from '../mongo';
import { registerServiceModels } from '../../lib/registerServiceModels';
getConnection().then(async (db) => {
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
// need to import StreamHub service after models are registered
const { StreamHub } = await import('./StreamHub');
api.registerService(new StreamHub());
});

View File

@ -43,6 +43,6 @@
// "emitDecoratorMetadata": true,
// "experimentalDecorators": true,
},
"include": ["./**/*", "../../../definition/externals/meteor/rocketchat-streamer.d.ts"],
"include": ["./**/*", "../../../definition/externals/meteor/rocketchat-streamer.d.ts", "../../../../../ee/apps/account-service/src/lib"],
"exclude": ["./dist", "./ecosystem.config.js", "../../../definition/methods"]
}

View File

@ -2,7 +2,6 @@ import EJSON from 'ejson';
import { Errors, Serializers, ServiceBroker } from 'moleculer';
import { pino } from 'pino';
import { api } from '../../../server/sdk/api';
import { isMeteorError, MeteorError } from '../../../server/sdk/errors';
import { NetworkBroker } from '../NetworkBroker';
@ -171,4 +170,4 @@ const network = new ServiceBroker({
},
});
api.setBroker(new NetworkBroker(network));
export const broker = new NetworkBroker(network);

View File

@ -3,9 +3,15 @@ import './engagementDashboard';
import './seatsCap';
import './services';
import './upsell';
import { api } from '../../../server/sdk/api';
import { isRunningMs } from '../../../server/lib/isRunningMs';
// only starts network broker if running in micro services mode
if (isRunningMs()) {
require('./broker');
(async () => {
const { broker } = await import('./broker');
api.setBroker(broker);
api.start();
})();
}

View File

@ -6,4 +6,5 @@ export const api = new Api();
if (!isRunningMs()) {
api.setBroker(new LocalBroker());
api.start();
}

View File

@ -64,4 +64,11 @@ export class Api implements IApiService {
nodeList(): Promise<IBrokerNode[]> {
return this.broker.nodeList();
}
async start(): Promise<void> {
if (!this.broker) {
throw new Error('No broker set to start.');
}
await this.broker.start();
}
}

View File

@ -13,6 +13,8 @@ export class LocalBroker implements IBroker {
private events = new EventEmitter();
private services = new Set<IServiceClass>();
async call(method: string, data: any): Promise<any> {
const result = await asyncLocalStorage.run(
{
@ -54,6 +56,8 @@ export class LocalBroker implements IBroker {
createService(instance: IServiceClass): void {
const namespace = instance.getName();
this.services.add(instance);
instance.created();
instance.getEvents().forEach((eventName) => {
@ -74,8 +78,6 @@ export class LocalBroker implements IBroker {
this.methods.set(`${namespace}.${method}`, i[method].bind(i));
}
instance.started();
}
async broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
@ -102,4 +104,8 @@ export class LocalBroker implements IBroker {
return instances.map(({ _id }) => ({ id: _id, available: true }));
}
async start(): Promise<void> {
await Promise.all([...this.services].map((service) => service.started()));
}
}

View File

@ -59,4 +59,5 @@ export interface IBroker {
broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
nodeList(): Promise<IBrokerNode[]>;
start(): Promise<void>;
}

View File

@ -1,7 +1,6 @@
import type { Db, Collection } from 'mongodb';
import mem from 'mem';
import type { IUser, IRole, IRoom, ISubscription } from '@rocket.chat/core-typings';
import { Subscriptions, Rooms, Users, Roles } from '@rocket.chat/models';
import { Subscriptions, Rooms, Users, Roles, Permissions } from '@rocket.chat/models';
import type { IAuthorization, RoomAccessValidator } from '../../sdk/types/IAuthorization';
import { ServiceClass } from '../../sdk/types/ServiceClass';
@ -15,8 +14,6 @@ import './canAccessRoomLivechat';
export class Authorization extends ServiceClass implements IAuthorization {
protected name = 'authorization';
private Permissions: Collection;
private getRolesCached = mem(this.getRoles.bind(this), {
maxAge: 1000,
cacheKey: JSON.stringify,
@ -27,11 +24,9 @@ export class Authorization extends ServiceClass implements IAuthorization {
...(process.env.TEST_MODE === 'true' && { maxAge: 1 }),
});
constructor(db: Db) {
constructor() {
super();
this.Permissions = db.collection('rocketchat_permissions');
const clearCache = (): void => {
mem.clear(this.getRolesCached);
mem.clear(this.rolesHasPermissionCached);
@ -148,7 +143,7 @@ export class Authorization extends ServiceClass implements IAuthorization {
return false;
}
const result = await this.Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } });
const result = await Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } });
return !!result;
}

View File

@ -50,6 +50,6 @@ if (!isRunningMs()) {
const { Authorization } = await import('./authorization/service');
api.registerService(new Presence());
api.registerService(new Authorization(db));
api.registerService(new Authorization());
})();
}

View File

@ -29,6 +29,8 @@ Meteor.startup(function () {
const desiredNodeVersionMajor = String(semver.parse(desiredNodeVersion).major);
return Meteor.setTimeout(function () {
const replicaSet = isRunningMs() ? 'Not required (running micro services)' : `${oplogEnabled ? 'Enabled' : 'Disabled'}`;
let msg = [
`Rocket.Chat Version: ${Info.version}`,
` NodeJS Version: ${process.versions.node} - ${process.arch}`,
@ -37,7 +39,7 @@ Meteor.startup(function () {
` Platform: ${process.platform}`,
` Process Port: ${process.env.PORT}`,
` Site URL: ${settings.get('Site_Url')}`,
` ReplicaSet OpLog: ${oplogEnabled ? 'Enabled' : 'Disabled'}`,
` ReplicaSet OpLog: ${replicaSet}`,
];
if (Info.commit && Info.commit.hash) {

View File

@ -26,9 +26,9 @@ services:
authorization-service:
build:
dockerfile: apps/meteor/ee/server/services/Dockerfile
dockerfile: ee/apps/authorization-service/Dockerfile
args:
SERVICE: authorization
SERVICE: authorization-service
image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG}
environment:
- 'MONGO_URL=${MONGO_URL}'
@ -41,9 +41,9 @@ services:
account-service:
build:
dockerfile: apps/meteor/ee/server/services/Dockerfile
dockerfile: ee/apps/account-service/Dockerfile
args:
SERVICE: account
SERVICE: account-service
image: ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${DOCKER_TAG}
environment:
- MONGO_URL=${MONGO_URL}
@ -92,9 +92,9 @@ services:
stream-hub-service:
build:
dockerfile: apps/meteor/ee/server/services/Dockerfile
dockerfile: ee/apps/stream-hub-service/Dockerfile
args:
SERVICE: stream-hub
SERVICE: stream-hub-service
image: ghcr.io/${LOWERCASE_REPOSITORY}/stream-hub-service:${DOCKER_TAG}
environment:
- MONGO_URL=${MONGO_URL}
@ -107,10 +107,6 @@ services:
nats:
image: nats:2.6-alpine
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '4222:4222'
traefik:
image: traefik:v2.8

View File

@ -0,0 +1,16 @@
{
"extends": ["@rocket.chat/eslint-config"],
"overrides": [
{
"files": ["**/*.spec.js", "**/*.spec.jsx"],
"env": {
"jest": true
}
}
],
"ignorePatterns": ["**/dist"],
"plugins": ["jest"],
"env": {
"jest/globals": true
}
}

View File

@ -0,0 +1,34 @@
FROM node:14.19.3-alpine
ARG SERVICE
WORKDIR /app
COPY ./packages/core-typings/package.json packages/core-typings/package.json
COPY ./packages/core-typings/dist packages/core-typings/dist
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json
COPY ./packages/rest-typings/dist packages/rest-typings/dist
COPY ./packages/model-typings/package.json packages/model-typings/package.json
COPY ./packages/model-typings/dist packages/model-typings/dist
COPY ./packages/models/package.json packages/models/package.json
COPY ./packages/models/dist packages/models/dist
COPY ./ee/apps/${SERVICE}/dist .
COPY ./package.json .
COPY ./yarn.lock .
COPY ./.yarnrc.yml .
COPY ./.yarn/plugins .yarn/plugins
COPY ./.yarn/releases .yarn/releases
COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json
ENV NODE_ENV=production \
PORT=3000
WORKDIR /app/ee/apps/${SERVICE}
RUN yarn workspaces focus --production
EXPOSE 3000 9458
CMD ["node", "src/service.js"]

View File

@ -0,0 +1,50 @@
{
"name": "@rocket.chat/account-service",
"private": true,
"version": "0.1.0",
"description": "Rocket.Chat Account service",
"scripts": {
"build": "tsc -p tsconfig.json",
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"keywords": [
"rocketchat"
],
"author": "Rocket.Chat",
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "next",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
"@rocket.chat/rest-typings": "workspace:^",
"@rocket.chat/string-helpers": "next",
"@types/node": "^14.18.21",
"bcrypt": "^5.0.1",
"ejson": "^2.2.2",
"eventemitter3": "^4.0.7",
"fibers": "^5.0.3",
"mem": "^8.1.1",
"moleculer": "^0.14.21",
"mongodb": "^4.3.1",
"nats": "^2.4.0",
"pino": "^8.4.2",
"polka": "^0.5.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@rocket.chat/eslint-config": "workspace:^",
"@types/bcrypt": "^5.0.0",
"@types/eslint": "^8",
"@types/polka": "^0.5.4",
"eslint": "^8.21.0",
"ts-node": "^10.9.1",
"typescript": "~4.5.5"
},
"main": "./dist/ee/apps/account-service/src/service.js",
"files": [
"/dist"
]
}

View File

@ -0,0 +1,53 @@
import { Settings } from '@rocket.chat/models';
import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass';
import type { IAccount, ILoginResult } from '../../../../apps/meteor/server/sdk/types/IAccount';
import { removeSession } from './lib/removeSession';
import { loginViaResume } from './lib/loginViaResume';
import { loginViaUsername } from './lib/loginViaUsername';
export class Account extends ServiceClass implements IAccount {
protected name = 'accounts';
private loginExpiration = 90;
constructor() {
super();
this.onEvent('watch.settings', async ({ clientAction, setting }): Promise<void> => {
if (clientAction === 'removed') {
return;
}
const { _id, value } = setting;
if (_id !== 'Accounts_LoginExpiration') {
return;
}
if (typeof value === 'number') {
this.loginExpiration = value;
}
});
}
async login({ resume, user, password }: { resume: string; user: { username: string }; password: string }): Promise<false | ILoginResult> {
if (resume) {
return loginViaResume(resume, this.loginExpiration);
}
if (user && password) {
return loginViaUsername(user, password, this.loginExpiration);
}
return false;
}
async logout({ userId, token }: { userId: string; token: string }): Promise<void> {
return removeSession(userId, token);
}
async started(): Promise<void> {
const expiry = await Settings.findOne({ _id: 'Accounts_LoginExpiration' }, { projection: { value: 1 } });
if (expiry?.value) {
this.loginExpiration = expiry.value as number;
}
}
}

View File

@ -0,0 +1,39 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { _hashLoginToken, _tokenExpiration } from './utils';
import type { ILoginResult } from '../../../../../apps/meteor/server/sdk/types/IAccount';
import { MeteorError } from '../../../../../apps/meteor/server/sdk/errors';
export async function loginViaResume(resume: string, loginExpiration: number): Promise<false | ILoginResult> {
const hashedToken = _hashLoginToken(resume);
const user = await Users.findOne<IUser>(
{
'services.resume.loginTokens.hashedToken': hashedToken,
},
{
projection: {
'services.resume.loginTokens': 1,
},
},
);
if (!user) {
return false;
}
const { when } = user.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken) || {};
const tokenExpires = when && _tokenExpiration(when, loginExpiration);
if (tokenExpires && new Date() >= tokenExpires) {
throw new MeteorError(403, 'Your session has expired. Please log in again.');
}
return {
uid: user._id,
token: resume,
hashedToken,
type: 'resume',
...(tokenExpires && { tokenExpires }),
};
}

View File

@ -0,0 +1,36 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { _generateStampedLoginToken, _hashStampedToken, _tokenExpiration, validatePassword } from './utils';
import type { ILoginResult } from '../../../../../apps/meteor/server/sdk/types/IAccount';
import { saveSession } from './saveSession';
export async function loginViaUsername(
{ username }: { username: string },
password: string,
loginExpiration: number,
): Promise<false | ILoginResult> {
const user = await Users.findOne<IUser>({ username }, { projection: { 'services.password.bcrypt': 1 } });
if (!user) {
return false;
}
const valid = user.services?.password?.bcrypt && validatePassword(password, user.services.password.bcrypt);
if (!valid) {
return false;
}
const newToken = _generateStampedLoginToken();
const hashedToken = _hashStampedToken(newToken);
await saveSession(user._id, hashedToken);
return {
uid: user._id,
token: newToken.token,
hashedToken: hashedToken.hashedToken,
tokenExpires: _tokenExpiration(newToken.when, loginExpiration),
type: 'password',
};
}

View File

@ -0,0 +1,14 @@
import { Users } from '@rocket.chat/models';
export async function removeSession(uid: string, loginToken: string): Promise<void> {
await Users.updateOne(
{ _id: uid },
{
$pull: {
'services.resume.loginTokens': {
$or: [{ hashedToken: loginToken }, { token: loginToken }],
},
},
},
);
}

View File

@ -0,0 +1,14 @@
import { Users } from '@rocket.chat/models';
import type { IHashedStampedToken } from './utils';
export async function saveSession(uid: string, newToken: IHashedStampedToken): Promise<void> {
await Users.updateOne(
{ _id: uid },
{
$push: {
'services.resume.loginTokens': newToken.hashedToken,
},
},
);
}

View File

@ -0,0 +1,33 @@
import type { Document } from 'mongodb';
import polka from 'polka';
import { api } from '../../../../apps/meteor/server/sdk/api';
import { broker } from '../../../../apps/meteor/ee/server/startup/broker';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
const PORT = process.env.PORT || 3033;
(async () => {
const db = await getConnection();
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
api.setBroker(broker);
// need to import service after models are registered
const { Account } = await import('./Account');
api.registerService(new Account());
await api.start();
polka()
.get('/health', async function (_req, res) {
await api.nodeList();
res.end('ok');
})
.listen(PORT);
})();

View File

@ -0,0 +1,31 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "es2018",
"lib": ["esnext", "dom"],
"allowJs": true,
"checkJs": false,
"incremental": true,
/* Strict Type-Checking Options */
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"strictFunctionTypes": false,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": false,
/* Module Resolution Options */
"outDir": "./dist",
"importsNotUsedAsValues": "preserve",
"declaration": false,
"declarationMap": false
},
"files": ["./src/service.ts"],
"include": ["../../../apps/meteor/definition/externals/meteor"],
"exclude": ["./dist"]
}

View File

@ -0,0 +1,16 @@
{
"extends": ["@rocket.chat/eslint-config"],
"overrides": [
{
"files": ["**/*.spec.js", "**/*.spec.jsx"],
"env": {
"jest": true
}
}
],
"ignorePatterns": ["**/dist"],
"plugins": ["jest"],
"env": {
"jest/globals": true
}
}

View File

@ -0,0 +1,34 @@
FROM node:14.19.3-alpine
ARG SERVICE
WORKDIR /app
COPY ./packages/core-typings/package.json packages/core-typings/package.json
COPY ./packages/core-typings/dist packages/core-typings/dist
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json
COPY ./packages/rest-typings/dist packages/rest-typings/dist
COPY ./packages/model-typings/package.json packages/model-typings/package.json
COPY ./packages/model-typings/dist packages/model-typings/dist
COPY ./packages/models/package.json packages/models/package.json
COPY ./packages/models/dist packages/models/dist
COPY ./ee/apps/${SERVICE}/dist .
COPY ./package.json .
COPY ./yarn.lock .
COPY ./.yarnrc.yml .
COPY ./.yarn/plugins .yarn/plugins
COPY ./.yarn/releases .yarn/releases
COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json
ENV NODE_ENV=production \
PORT=3000
WORKDIR /app/ee/apps/${SERVICE}
RUN yarn workspaces focus --production
EXPOSE 3000 9458
CMD ["node", "src/service.js"]

View File

@ -0,0 +1,47 @@
{
"name": "@rocket.chat/authorization-service",
"private": true,
"version": "0.1.0",
"description": "Rocket.Chat Authorization service",
"scripts": {
"build": "tsc -p tsconfig.json",
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"keywords": [
"rocketchat"
],
"author": "Rocket.Chat",
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "next",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
"@rocket.chat/rest-typings": "workspace:^",
"@rocket.chat/string-helpers": "next",
"@types/node": "^14.18.21",
"ejson": "^2.2.2",
"eventemitter3": "^4.0.7",
"fibers": "^5.0.3",
"mem": "^8.1.1",
"moleculer": "^0.14.21",
"mongodb": "^4.3.1",
"nats": "^2.4.0",
"pino": "^8.4.2",
"polka": "^0.5.2"
},
"devDependencies": {
"@rocket.chat/eslint-config": "workspace:^",
"@types/eslint": "^8",
"@types/polka": "^0.5.4",
"eslint": "^8.21.0",
"ts-node": "^10.9.1",
"typescript": "~4.5.5"
},
"main": "./dist/ee/apps/authorization-service/src/service.js",
"files": [
"/dist"
]
}

View File

@ -0,0 +1,33 @@
import type { Document } from 'mongodb';
import polka from 'polka';
import { api } from '../../../../apps/meteor/server/sdk/api';
import { broker } from '../../../../apps/meteor/ee/server/startup/broker';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
const PORT = process.env.PORT || 3034;
(async () => {
const db = await getConnection();
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
api.setBroker(broker);
// need to import service after models are registered
const { Authorization } = await import('../../../../apps/meteor/server/services/authorization/service');
api.registerService(new Authorization());
await api.start();
polka()
.get('/health', async function (_req, res) {
await api.nodeList();
res.end('ok');
})
.listen(PORT);
})();

View File

@ -0,0 +1,31 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "es2018",
"lib": ["esnext", "dom"],
"allowJs": true,
"checkJs": false,
"incremental": true,
/* Strict Type-Checking Options */
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"strictFunctionTypes": false,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": false,
/* Module Resolution Options */
"outDir": "./dist",
"importsNotUsedAsValues": "preserve",
"declaration": false,
"declarationMap": false
},
"files": ["./src/service.ts"],
"include": ["../../../apps/meteor/definition/externals/meteor"],
"exclude": ["./dist"]
}

View File

@ -12,8 +12,6 @@ COPY ./packages/model-typings/package.json packages/model-typings/package.json
COPY ./packages/model-typings/dist packages/model-typings/dist
COPY ./packages/models/package.json packages/models/package.json
COPY ./packages/models/dist packages/models/dist
COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json
COPY ./packages/ui-contexts/dist packages/ui-contexts/dist
COPY ./ee/apps/${SERVICE}/dist .

View File

@ -22,16 +22,16 @@
"@rocket.chat/models": "workspace:^",
"@rocket.chat/rest-typings": "workspace:^",
"@rocket.chat/string-helpers": "next",
"@rocket.chat/ui-contexts": "workspace:^",
"colorette": "^1.4.0",
"ejson": "^2.2.2",
"eventemitter3": "^4.0.7",
"fibers": "^5.0.1",
"fibers": "^5.0.3",
"jaeger-client": "^3.19.0",
"moleculer": "^0.14.21",
"mongodb": "^4.3.1",
"nats": "^2.4.0",
"pino": "^7.11.0",
"polka": "^0.5.2",
"sharp": "^0.30.7",
"underscore": "^1.13.4",
"uuid": "^7.0.3",
@ -43,6 +43,7 @@
"@types/eslint": "^8",
"@types/meteor": "2.7.1",
"@types/node": "^14.18.21",
"@types/polka": "^0.5.4",
"@types/sharp": "^0.30.4",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",

View File

@ -1,68 +1,29 @@
import type { IncomingMessage, RequestOptions, ServerResponse } from 'http';
import http from 'http';
import url from 'url';
import crypto from 'crypto';
import polka from 'polka';
import WebSocket from 'ws';
import type { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module';
import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module';
import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module';
import { MeteorService, Presence } from '../../../../apps/meteor/server/sdk';
import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass';
import { api } from '../../../../apps/meteor/server/sdk/api';
import { Client } from './Client';
import { events, server } from './configureServer';
import { DDP_EVENTS } from './constants';
import { Autoupdate } from './lib/Autoupdate';
import { notifications } from './streams';
import { proxy } from './proxy';
const { PORT: port = 4000 } = process.env;
const proxy = function (req: IncomingMessage, res: ServerResponse): void {
req.pause();
const options: RequestOptions = url.parse(req.url || '');
options.headers = req.headers;
options.method = req.method;
options.agent = false;
options.hostname = 'localhost';
options.port = 3000;
const connector = http.request(options, function (serverResponse) {
serverResponse.pause();
if (serverResponse.statusCode) {
res.writeHead(serverResponse.statusCode, serverResponse.headers);
}
serverResponse.pipe(res);
serverResponse.resume();
});
req.pipe(connector);
req.resume();
};
const httpServer = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
if (process.env.NODE_ENV !== 'production' && !/^\/sockjs\/info\?cb=/.test(req.url || '')) {
return proxy(req, res);
// res.writeHead(404);
// return res.end();
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":666}');
});
httpServer.listen(port);
const wss = new WebSocket.Server({ server: httpServer });
wss.on('connection', (ws, req) => new Client(ws, req.url !== '/websocket', req));
const { PORT = 4000 } = process.env;
export class DDPStreamer extends ServiceClass {
protected name = 'streamer';
constructor() {
private app?: polka.Polka;
private wss?: WebSocket.Server;
constructor(notifications: NotificationsModule) {
super();
new ListenersModule(this, notifications);
@ -90,15 +51,6 @@ export class DDPStreamer extends ServiceClass {
});
}
async started(): Promise<void> {
// TODO this call creates a dependency to MeteorService, should it be a hard dependency? or can this call fail and be ignored?
const versions = await MeteorService.getAutoUpdateClientVersions();
Object.keys(versions).forEach((key) => {
Autoupdate.updateVersion(versions[key]);
});
}
async created(): Promise<void> {
if (!this.context) {
return;
@ -147,13 +99,13 @@ export class DDPStreamer extends ServiceClass {
const { userId, connection } = info;
Presence.newConnection(userId, connection.id, nodeID);
api.broadcast('accounts.login', { userId, connection });
this.api.broadcast('accounts.login', { userId, connection });
});
server.on(DDP_EVENTS.LOGGEDOUT, (info) => {
const { userId, connection } = info;
api.broadcast('accounts.logout', { userId, connection });
this.api.broadcast('accounts.logout', { userId, connection });
if (!userId) {
return;
@ -164,7 +116,7 @@ export class DDPStreamer extends ServiceClass {
server.on(DDP_EVENTS.DISCONNECTED, (info) => {
const { userId, connection } = info;
api.broadcast('socket.disconnected', connection);
this.api.broadcast('socket.disconnected', connection);
if (!userId) {
return;
@ -173,7 +125,45 @@ export class DDPStreamer extends ServiceClass {
});
server.on(DDP_EVENTS.CONNECTED, ({ connection }) => {
api.broadcast('socket.connected', connection);
this.api.broadcast('socket.connected', connection);
});
}
async started(): Promise<void> {
// TODO this call creates a dependency to MeteorService, should it be a hard dependency? or can this call fail and be ignored?
const versions = await MeteorService.getAutoUpdateClientVersions();
Object.keys(versions).forEach((key) => {
Autoupdate.updateVersion(versions[key]);
});
this.app = polka()
.use(proxy())
.get('/health', async (_req, res) => {
await this.api.nodeList();
res.end('ok');
})
.get('*', function (_req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(`{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":${crypto.randomBytes(4).readUInt32LE(0)},"ms":true}`);
})
.listen(PORT);
this.wss = new WebSocket.Server({ server: this.app.server });
this.wss.on('connection', (ws, req) => new Client(ws, req.url !== '/websocket', req));
}
async stopped(): Promise<void> {
this.wss?.clients.forEach(function (client) {
client.terminate();
});
this.app?.server?.close();
this.wss?.close();
}
}

View File

@ -0,0 +1,41 @@
import type { IncomingMessage, RequestOptions, ServerResponse } from 'http';
import http from 'http';
import url from 'url';
import type polka from 'polka';
const isProdEnv = process.env.NODE_ENV === 'production';
const skipProxyPaths = [/^\/sockjs\/info\?cb=/, /^\/health/];
export function proxy(): (req: IncomingMessage, res: ServerResponse, next: polka.Next) => void {
if (isProdEnv) {
return (_req, _res, next) => next();
}
return (req, res, next) => {
if (skipProxyPaths.some((regex) => regex.test(req.url || ''))) {
return next();
}
req.pause();
const options: RequestOptions = url.parse(req.url || '');
options.headers = req.headers;
options.method = req.method;
options.agent = false;
options.hostname = 'localhost';
options.port = 3000;
const connector = http.request(options, function (serverResponse) {
serverResponse.pause();
if (serverResponse.statusCode) {
res.writeHead(serverResponse.statusCode, serverResponse.headers);
}
serverResponse.pipe(res);
serverResponse.resume();
});
req.pipe(connector);
req.resume();
};
}

View File

@ -1,6 +1,29 @@
import '../../../../apps/meteor/ee/server/startup/broker';
import type { Document } from 'mongodb';
import { api } from '../../../../apps/meteor/server/sdk/api';
import { DDPStreamer } from './DDPStreamer';
import { broker } from '../../../../apps/meteor/ee/server/startup/broker';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
api.registerService(new DDPStreamer());
(async () => {
const db = await getConnection();
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
api.setBroker(broker);
// need to import service after models are registered
const { NotificationsModule } = await import('../../../../apps/meteor/server/modules/notifications/notifications.module');
const { DDPStreamer } = await import('./DDPStreamer');
const { Stream } = await import('./Streamer');
const notifications = new NotificationsModule(Stream);
notifications.configure();
api.registerService(new DDPStreamer(notifications));
await api.start();
})();

View File

@ -1,16 +0,0 @@
import type { Document } from 'mongodb';
import { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module';
import { Stream } from './Streamer';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
export const notifications = new NotificationsModule(Stream);
getConnection().then(async (db) => {
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
notifications.configure();
});

View File

@ -15,13 +15,16 @@
],
"author": "Rocket.Chat",
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "next",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
"@rocket.chat/presence": "workspace:^",
"@rocket.chat/string-helpers": "next",
"@types/node": "^14.18.21",
"ejson": "^2.2.2",
"eventemitter3": "^4.0.7",
"fibers": "^5.0.1",
"fibers": "^5.0.3",
"moleculer": "^0.14.21",
"mongodb": "^4.3.1",
"nats": "^2.4.0",

View File

@ -1,28 +1,33 @@
import type { Document } from 'mongodb';
import polka from 'polka';
import '../../../../apps/meteor/ee/server/startup/broker';
import { api } from '../../../../apps/meteor/server/sdk/api';
import { broker } from '../../../../apps/meteor/ee/server/startup/broker';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
const PORT = process.env.PORT || 3031;
getConnection().then(async (db) => {
(async () => {
const db = await getConnection();
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
api.setBroker(broker);
// need to import Presence service after models are registered
const { Presence } = await import('@rocket.chat/presence');
api.registerService(new Presence());
await api.start();
polka()
.get('/health', async function (_req, res) {
await api.nodeList();
res.end('ok');
})
.listen(PORT);
});
})();

View File

@ -26,6 +26,6 @@
"declarationMap": false
},
"files": ["./src/service.ts"],
"include": ["../../../apps/meteor/definition"],
"include": ["../../../apps/meteor/definition/externals/meteor"],
"exclude": ["./dist"]
}

View File

@ -0,0 +1,16 @@
{
"extends": ["@rocket.chat/eslint-config"],
"overrides": [
{
"files": ["**/*.spec.js", "**/*.spec.jsx"],
"env": {
"jest": true
}
}
],
"ignorePatterns": ["**/dist"],
"plugins": ["jest"],
"env": {
"jest/globals": true
}
}

View File

@ -0,0 +1,34 @@
FROM node:14.19.3-alpine
ARG SERVICE
WORKDIR /app
COPY ./packages/core-typings/package.json packages/core-typings/package.json
COPY ./packages/core-typings/dist packages/core-typings/dist
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json
COPY ./packages/rest-typings/dist packages/rest-typings/dist
COPY ./packages/model-typings/package.json packages/model-typings/package.json
COPY ./packages/model-typings/dist packages/model-typings/dist
COPY ./packages/models/package.json packages/models/package.json
COPY ./packages/models/dist packages/models/dist
COPY ./ee/apps/${SERVICE}/dist .
COPY ./package.json .
COPY ./yarn.lock .
COPY ./.yarnrc.yml .
COPY ./.yarn/plugins .yarn/plugins
COPY ./.yarn/releases .yarn/releases
COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json
ENV NODE_ENV=production \
PORT=3000
WORKDIR /app/ee/apps/${SERVICE}
RUN yarn workspaces focus --production
EXPOSE 3000 9458
CMD ["node", "src/service.js"]

View File

@ -0,0 +1,47 @@
{
"name": "@rocket.chat/stream-hub-service",
"private": true,
"version": "0.1.0",
"description": "Rocket.Chat Stream Hub service",
"scripts": {
"build": "tsc -p tsconfig.json",
"ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"keywords": [
"rocketchat"
],
"author": "Rocket.Chat",
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "next",
"@rocket.chat/model-typings": "workspace:^",
"@rocket.chat/models": "workspace:^",
"@rocket.chat/string-helpers": "next",
"@types/node": "^14.18.21",
"ejson": "^2.2.2",
"eventemitter3": "^4.0.7",
"fibers": "^5.0.3",
"mem": "^8.1.1",
"moleculer": "^0.14.21",
"mongodb": "^4.3.1",
"nats": "^2.4.0",
"pino": "^8.4.2",
"polka": "^0.5.2"
},
"devDependencies": {
"@rocket.chat/eslint-config": "workspace:^",
"@types/bcrypt": "^5.0.0",
"@types/eslint": "^8",
"@types/polka": "^0.5.4",
"eslint": "^8.21.0",
"ts-node": "^10.9.1",
"typescript": "~4.5.5"
},
"main": "./dist/ee/apps/stream-hub-service/src/service.js",
"files": [
"/dist"
]
}

View File

@ -0,0 +1,22 @@
import type { Db } from 'mongodb';
import type { IServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass';
import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass';
import { initWatchers } from '../../../../apps/meteor/server/modules/watchers/watchers.module';
import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher';
export class StreamHub extends ServiceClass implements IServiceClass {
protected name = 'hub';
constructor(private db: Db) {
super();
}
async created(): Promise<void> {
const watcher = new DatabaseWatcher({ db: this.db });
initWatchers(watcher, this.api.broadcast.bind(this.api));
watcher.watch();
}
}

View File

@ -0,0 +1,33 @@
import type { Document } from 'mongodb';
import polka from 'polka';
import { api } from '../../../../apps/meteor/server/sdk/api';
import { broker } from '../../../../apps/meteor/ee/server/startup/broker';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';
const PORT = process.env.PORT || 3035;
(async () => {
const db = await getConnection();
const trash = await getCollection<Document>(Collections.Trash);
registerServiceModels(db, trash);
api.setBroker(broker);
// need to import service after models are registered
const { StreamHub } = await import('./StreamHub');
api.registerService(new StreamHub(db));
await api.start();
polka()
.get('/health', async function (_req, res) {
await api.nodeList();
res.end('ok');
})
.listen(PORT);
})();

View File

@ -0,0 +1,31 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "es2018",
"lib": ["esnext", "dom"],
"allowJs": true,
"checkJs": false,
"incremental": true,
/* Strict Type-Checking Options */
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"strictFunctionTypes": false,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": false,
/* Module Resolution Options */
"outDir": "./dist",
"importsNotUsedAsValues": "preserve",
"declaration": false,
"declarationMap": false
},
"files": ["./src/service.ts"],
"include": ["../../../apps/meteor/definition/externals/meteor"],
"exclude": ["./dist"]
}

View File

@ -76,6 +76,7 @@ export interface IStats {
};
instanceCount: number;
oplogEnabled: boolean;
msEnabled: boolean;
mongoVersion: string;
mongoStorageEngine: string;
pushQueue: number;

View File

@ -9,7 +9,6 @@
"@rocket.chat/apps-engine": "^1.32.0",
"@rocket.chat/eslint-config": "workspace:^",
"@rocket.chat/rest-typings": "workspace:^",
"@rocket.chat/ui-contexts": "workspace:^",
"@types/node": "^14.18.21",
"babel-jest": "^29.0.3",
"eslint": "^8.21.0",

119
yarn.lock
View File

@ -5462,6 +5462,38 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/account-service@workspace:ee/apps/account-service":
version: 0.0.0-use.local
resolution: "@rocket.chat/account-service@workspace:ee/apps/account-service"
dependencies:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/emitter": next
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/model-typings": "workspace:^"
"@rocket.chat/models": "workspace:^"
"@rocket.chat/rest-typings": "workspace:^"
"@rocket.chat/string-helpers": next
"@types/bcrypt": ^5.0.0
"@types/eslint": ^8
"@types/node": ^14.18.21
"@types/polka": ^0.5.4
bcrypt: ^5.0.1
ejson: ^2.2.2
eslint: ^8.21.0
eventemitter3: ^4.0.7
fibers: ^5.0.3
mem: ^8.1.1
moleculer: ^0.14.21
mongodb: ^4.3.1
nats: ^2.4.0
pino: ^8.4.2
polka: ^0.5.2
ts-node: ^10.9.1
typescript: ~4.5.5
uuid: ^9.0.0
languageName: unknown
linkType: soft
"@rocket.chat/agenda@workspace:^, @rocket.chat/agenda@workspace:packages/agenda":
version: 0.0.0-use.local
resolution: "@rocket.chat/agenda@workspace:packages/agenda"
@ -5542,6 +5574,35 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/authorization-service@workspace:ee/apps/authorization-service":
version: 0.0.0-use.local
resolution: "@rocket.chat/authorization-service@workspace:ee/apps/authorization-service"
dependencies:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/emitter": next
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/model-typings": "workspace:^"
"@rocket.chat/models": "workspace:^"
"@rocket.chat/rest-typings": "workspace:^"
"@rocket.chat/string-helpers": next
"@types/eslint": ^8
"@types/node": ^14.18.21
"@types/polka": ^0.5.4
ejson: ^2.2.2
eslint: ^8.21.0
eventemitter3: ^4.0.7
fibers: ^5.0.3
mem: ^8.1.1
moleculer: ^0.14.21
mongodb: ^4.3.1
nats: ^2.4.0
pino: ^8.4.2
polka: ^0.5.2
ts-node: ^10.9.1
typescript: ~4.5.5
languageName: unknown
linkType: soft
"@rocket.chat/cas-validate@workspace:^, @rocket.chat/cas-validate@workspace:packages/cas-validate":
version: 0.0.0-use.local
resolution: "@rocket.chat/cas-validate@workspace:packages/cas-validate"
@ -5605,11 +5666,11 @@ __metadata:
"@rocket.chat/models": "workspace:^"
"@rocket.chat/rest-typings": "workspace:^"
"@rocket.chat/string-helpers": next
"@rocket.chat/ui-contexts": "workspace:^"
"@types/ejson": ^2.2.0
"@types/eslint": ^8
"@types/meteor": 2.7.1
"@types/node": ^14.18.21
"@types/polka": ^0.5.4
"@types/sharp": ^0.30.4
"@types/uuid": ^8.3.4
"@types/ws": ^8.5.3
@ -5617,13 +5678,14 @@ __metadata:
ejson: ^2.2.2
eslint: ^8.22.0
eventemitter3: ^4.0.7
fibers: ^5.0.1
fibers: ^5.0.3
jaeger-client: ^3.19.0
moleculer: ^0.14.21
mongodb: ^4.3.1
nats: ^2.4.0
pino: ^7.11.0
pino-pretty: ^7.6.1
polka: ^0.5.2
sharp: ^0.30.7
ts-node: ^10.9.1
typescript: ~4.5.5
@ -6454,8 +6516,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "@rocket.chat/presence-service@workspace:ee/apps/presence-service"
dependencies:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/emitter": next
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/model-typings": "workspace:^"
"@rocket.chat/models": "workspace:^"
"@rocket.chat/presence": "workspace:^"
"@rocket.chat/string-helpers": next
"@types/eslint": ^8
@ -6464,7 +6529,7 @@ __metadata:
ejson: ^2.2.2
eslint: ^8.21.0
eventemitter3: ^4.0.7
fibers: ^5.0.1
fibers: ^5.0.3
moleculer: ^0.14.21
mongodb: ^4.3.1
nats: ^2.4.0
@ -6487,7 +6552,6 @@ __metadata:
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/models": "workspace:^"
"@rocket.chat/rest-typings": "workspace:^"
"@rocket.chat/ui-contexts": "workspace:^"
"@types/node": ^14.18.21
babel-jest: ^29.0.3
eslint: ^8.21.0
@ -6538,6 +6602,35 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/stream-hub-service@workspace:ee/apps/stream-hub-service":
version: 0.0.0-use.local
resolution: "@rocket.chat/stream-hub-service@workspace:ee/apps/stream-hub-service"
dependencies:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/emitter": next
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/model-typings": "workspace:^"
"@rocket.chat/models": "workspace:^"
"@rocket.chat/string-helpers": next
"@types/bcrypt": ^5.0.0
"@types/eslint": ^8
"@types/node": ^14.18.21
"@types/polka": ^0.5.4
ejson: ^2.2.2
eslint: ^8.21.0
eventemitter3: ^4.0.7
fibers: ^5.0.3
mem: ^8.1.1
moleculer: ^0.14.21
mongodb: ^4.3.1
nats: ^2.4.0
pino: ^8.4.2
polka: ^0.5.2
ts-node: ^10.9.1
typescript: ~4.5.5
languageName: unknown
linkType: soft
"@rocket.chat/string-helpers@npm:next":
version: 0.31.19-dev.19
resolution: "@rocket.chat/string-helpers@npm:0.31.19-dev.19"
@ -18513,6 +18606,15 @@ __metadata:
languageName: node
linkType: hard
"fibers@npm:^5.0.3":
version: 5.0.3
resolution: "fibers@npm:5.0.3"
dependencies:
detect-libc: ^1.0.3
checksum: d66c5e18a911aab3480b846e1c837e5c7cfacb27a2a5fe512919865eaecef33cdd4abc14d777191a6a93473dc52356d48549c91a2a7b8b3450544c44104b23f3
languageName: node
linkType: hard
"figgy-pudding@npm:^3.5.1":
version: 3.5.2
resolution: "figgy-pudding@npm:3.5.2"
@ -35808,6 +35910,15 @@ __metadata:
languageName: node
linkType: hard
"uuid@npm:^9.0.0":
version: 9.0.0
resolution: "uuid@npm:9.0.0"
bin:
uuid: dist/bin/uuid
checksum: 8dd2c83c43ddc7e1c71e36b60aea40030a6505139af6bee0f382ebcd1a56f6cd3028f7f06ffb07f8cf6ced320b76aea275284b224b002b289f89fe89c389b028
languageName: node
linkType: hard
"v8-compile-cache-lib@npm:^3.0.1":
version: 3.0.1
resolution: "v8-compile-cache-lib@npm:3.0.1"