Audit logging, secrets manager completion, sns and sqs wrapping up
This commit is contained in:
parent
07d3841cd7
commit
ee0babc2e3
|
|
@ -13,6 +13,7 @@
|
|||
"@nestjs/core": "^9.3.10",
|
||||
"@nestjs/platform-express": "^9.3.10",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"class-transformer": "^0.5.1",
|
||||
"joi": "^17.9.0",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as Joi from 'joi';
|
|||
export type AwsProperties = {
|
||||
accountId: string;
|
||||
region: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export enum Format {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BadRequestException, Body, Controller, Get, Inject, Post, Headers, Header } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Inject, Post, Headers, Header, Req, HttpStatus, HttpCode, UseInterceptors } from '@nestjs/common';
|
||||
import { ActionHandlers } from './app.constants';
|
||||
import * as Joi from 'joi';
|
||||
import { Action } from './action.enum';
|
||||
|
|
@ -7,6 +7,8 @@ import * as js2xmlparser from 'js2xmlparser';
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { CommonConfig } from './config/common-config.interface';
|
||||
import * as uuid from 'uuid';
|
||||
import { Request } from 'express';
|
||||
import { AuditInterceptor } from './audit/audit.interceptor';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
|
|
@ -18,8 +20,10 @@ export class AppController {
|
|||
) {}
|
||||
|
||||
@Post()
|
||||
@Header('x-amzn-RequestId', uuid.v4())
|
||||
@HttpCode(200)
|
||||
@UseInterceptors(AuditInterceptor)
|
||||
async post(
|
||||
@Req() request: Request,
|
||||
@Body() body: Record<string, any>,
|
||||
@Headers() headers: Record<string, any>,
|
||||
) {
|
||||
|
|
@ -29,7 +33,7 @@ export class AppController {
|
|||
return o;
|
||||
}, {})
|
||||
|
||||
const queryParams = { ...body, ...lowerCasedHeaders };
|
||||
const queryParams = { __path: request.path, ...body, ...lowerCasedHeaders };
|
||||
console.log({queryParams})
|
||||
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
||||
|
||||
|
|
@ -50,7 +54,11 @@ export class AppController {
|
|||
throw new BadRequestException(validatorError);
|
||||
}
|
||||
|
||||
const awsProperties = { accountId: this.configService.get('AWS_ACCOUNT_ID'), region: this.configService.get('AWS_REGION') };
|
||||
const awsProperties = {
|
||||
accountId: this.configService.get('AWS_ACCOUNT_ID'),
|
||||
region: this.configService.get('AWS_REGION'),
|
||||
host: this.configService.get('HOST'),
|
||||
};
|
||||
|
||||
const jsonResponse = await handler.getResponse(validQueryParams, awsProperties);
|
||||
if (handler.format === Format.Xml) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import { AppController } from './app.controller';
|
|||
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
|
||||
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
|
||||
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
|
||||
import { SqsModule } from './sqs/sqs.module';
|
||||
import { SqsHandlers } from './sqs/sqs.constants';
|
||||
import { Audit } from './audit/audit.entity';
|
||||
import { AuditInterceptor } from './audit/audit.interceptor';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -28,19 +32,23 @@ import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.consta
|
|||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
}),
|
||||
}),
|
||||
SnsModule,
|
||||
TypeOrmModule.forFeature([Audit]),
|
||||
SecretsManagerModule,
|
||||
SnsModule,
|
||||
SqsModule,
|
||||
AwsSharedEntitiesModule,
|
||||
],
|
||||
controllers: [
|
||||
AppController,
|
||||
],
|
||||
providers: [
|
||||
AuditInterceptor,
|
||||
{
|
||||
provide: ActionHandlers,
|
||||
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
|
||||
inject: [
|
||||
SnsHandlers,
|
||||
SqsHandlers,
|
||||
SecretsManagerHandlers,
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('audit')
|
||||
export class Audit extends BaseEntity {
|
||||
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
action: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
request: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
response: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Audit } from './audit.entity';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Audit)
|
||||
private readonly auditRepo: Repository<Audit>,
|
||||
) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
|
||||
|
||||
const requestId = uuid.v4();
|
||||
const httpContext = context.switchToHttp();
|
||||
|
||||
const request = httpContext.getRequest();
|
||||
const targetHeaderKey = Object.keys(request.headers).find( k => k.toLocaleLowerCase() === 'x-amz-target');
|
||||
const action = request.headers[targetHeaderKey] ? request.headers[targetHeaderKey] : request.body.Action;
|
||||
|
||||
const response = context.switchToHttp().getResponse();
|
||||
|
||||
response.header('x-amzn-RequestId', requestId);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(async (data) => {
|
||||
await this.auditRepo.create({
|
||||
id: requestId,
|
||||
action,
|
||||
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
||||
response: JSON.stringify(data),
|
||||
}).save();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import { Repository } from 'typeorm';
|
|||
import { Attribute } from './attributes.entity';
|
||||
import { CreateAttributeDto } from './create-attribute.dto';
|
||||
|
||||
const ResourcePolicyName = 'ResourcePolicy';
|
||||
|
||||
@Injectable()
|
||||
export class AttributesService {
|
||||
|
||||
|
|
@ -16,6 +18,14 @@ export class AttributesService {
|
|||
return await this.repo.find({ where: { arn }});
|
||||
}
|
||||
|
||||
async getResourcePolicyByArn(arn: string): Promise<Attribute> {
|
||||
return await this.repo.findOne({ where: { arn, name: ResourcePolicyName }});
|
||||
}
|
||||
|
||||
async createResourcePolicy(arn: string, value: string): Promise<Attribute> {
|
||||
return await this.create({arn, value, name: ResourcePolicyName });
|
||||
}
|
||||
|
||||
async create(dto: CreateAttributeDto): Promise<Attribute> {
|
||||
return await this.repo.save(dto);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ export interface CommonConfig {
|
|||
DB_DATABASE: string;
|
||||
DB_LOGGING?: boolean;
|
||||
DB_SYNCHRONIZE?: boolean;
|
||||
HOST: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { CommonConfig } from "./common-config.interface";
|
|||
export default (): CommonConfig => ({
|
||||
AWS_ACCOUNT_ID: '123456789012',
|
||||
AWS_REGION: 'us-east-1',
|
||||
DB_DATABASE: ':memory:', // 'local-aws.sqlite', // :memory:
|
||||
// DB_DATABASE: ':memory:',
|
||||
DB_DATABASE: 'local-aws.sqlite',
|
||||
DB_LOGGING: true,
|
||||
DB_SYNCHRONIZE: true,
|
||||
HOST: 'http://localhost:8081',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
export interface CreateSecretDto {
|
||||
versionId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
secretString?: string;
|
||||
accountId: string;
|
||||
region: string;
|
||||
}
|
||||
|
|
@ -5,21 +5,20 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
|
|||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { Secret } from './secret.entity';
|
||||
import * as uuid from 'uuid';
|
||||
import { SecretService } from './secret.service';
|
||||
|
||||
type QueryParams = {
|
||||
Name: string;
|
||||
Description: string;
|
||||
SecretString: string;
|
||||
ClientRequestToken: string;
|
||||
ClientRequestToken?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Secret)
|
||||
private readonly secretRepo: Repository<Secret>,
|
||||
private readonly secretService: SecretService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
|
@ -30,21 +29,21 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
|
|||
Name: Joi.string().required(),
|
||||
Description: Joi.string().allow('', null),
|
||||
SecretString: Joi.string().allow('', null),
|
||||
ClientRequestToken: Joi.string().required(),
|
||||
ClientRequestToken: Joi.string(),
|
||||
});
|
||||
|
||||
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params;
|
||||
|
||||
const secret = await this.secretRepo.create({
|
||||
const secret = await this.secretService.create({
|
||||
versionId: ClientRequestToken,
|
||||
description,
|
||||
name,
|
||||
secretString,
|
||||
accountId: awsProperties.accountId,
|
||||
region: awsProperties.region,
|
||||
}).save();
|
||||
});
|
||||
|
||||
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,9 @@ export class DescribeSecretHandler extends AbstractActionHandler {
|
|||
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
|
||||
|
||||
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const parts = SecretId.split(':');
|
||||
const name = parts.length > 1 ? parts[-1] : SecretId;
|
||||
|
||||
const secret = await this.secretRepo.findOne({ where: { name } });
|
||||
const name = Secret.getNameFromSecretId(SecretId);
|
||||
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
||||
|
||||
if (!secret) {
|
||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||
|
|
@ -42,19 +40,18 @@ export class DescribeSecretHandler extends AbstractActionHandler {
|
|||
|
||||
return {
|
||||
"ARN": secret.arn,
|
||||
"CreatedDate": new Date(secret.createdAt).getMilliseconds(),
|
||||
"DeletedDate": 0,
|
||||
"CreatedDate": new Date(secret.createdAt).toISOString(),
|
||||
"DeletedDate": null,
|
||||
"Description": secret.description,
|
||||
"KmsKeyId": "",
|
||||
"LastAccessedDate": new Date().getMilliseconds(),
|
||||
"LastChangedDate": new Date(secret.createdAt).getMilliseconds(),
|
||||
"LastRotatedDate": 0,
|
||||
"LastChangedDate": new Date(secret.createdAt).toISOString(),
|
||||
"LastRotatedDate": null,
|
||||
"Name": secret.name,
|
||||
"OwningService": secret.accountId,
|
||||
"PrimaryRegion": secret.region,
|
||||
"ReplicationStatus": [],
|
||||
"RotationEnabled": false,
|
||||
"Tags": listOfTagPairs,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { Secret } from './secret.entity';
|
||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||
|
||||
type QueryParams = {
|
||||
SecretId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GetResourcePolicyHandler extends AbstractActionHandler {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Secret)
|
||||
private readonly secretRepo: Repository<Secret>,
|
||||
private readonly attributesService: AttributesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Json;
|
||||
action = Action.SecretsManagerGetResourcePolicy;
|
||||
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
|
||||
|
||||
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const name = Secret.getNameFromSecretId(SecretId);
|
||||
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
||||
|
||||
if (!secret) {
|
||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||
}
|
||||
|
||||
const attribute = await this.attributesService.getResourcePolicyByArn(secret.arn);
|
||||
return {
|
||||
ARN: secret.arn,
|
||||
Name: secret.name,
|
||||
ResourcePolicy: attribute?.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { Secret } from './secret.entity';
|
||||
import { SecretService } from './secret.service';
|
||||
|
||||
type QueryParams = {
|
||||
SecretId: string;
|
||||
VersionId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GetSecretValueHandler extends AbstractActionHandler {
|
||||
|
||||
constructor(
|
||||
private readonly secretService: SecretService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Json;
|
||||
action = Action.SecretsManagerGetSecretValue;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
SecretId: Joi.string().required(),
|
||||
VersionId: Joi.string().allow(null, ''),
|
||||
});
|
||||
|
||||
protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const name = Secret.getNameFromSecretId(SecretId);
|
||||
const secret = VersionId ?
|
||||
await this.secretService.findByNameAndVersion(name, VersionId) :
|
||||
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||
|
||||
if (!secret) {
|
||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||
}
|
||||
|
||||
return {
|
||||
ARN: secret.arn,
|
||||
CreatedDate: secret.createdAt,
|
||||
Name: secret.name,
|
||||
SecretString: secret.secretString,
|
||||
VersionId: secret.versionId,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { Secret } from './secret.entity';
|
||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||
|
||||
type QueryParams = {
|
||||
SecretId: string;
|
||||
ResourcePolicy: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PutResourcePolicyHandler extends AbstractActionHandler {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Secret)
|
||||
private readonly secretRepo: Repository<Secret>,
|
||||
private readonly attributesService: AttributesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Json;
|
||||
action = Action.SecretsManagerPutResourcePolicy;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
SecretId: Joi.string().required(),
|
||||
ResourcePolicy: Joi.string().required(),
|
||||
});
|
||||
|
||||
protected async handle({ SecretId, ResourcePolicy }: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const name = Secret.getNameFromSecretId(SecretId);
|
||||
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
||||
|
||||
if (!secret) {
|
||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||
}
|
||||
|
||||
await this.attributesService.createResourcePolicy(secret.arn, ResourcePolicy);
|
||||
return {
|
||||
ARN: secret.arn,
|
||||
Name: secret.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { Secret } from './secret.entity';
|
||||
import { SecretService } from './secret.service';
|
||||
|
||||
type QueryParams = {
|
||||
ClientRequestToken?: string;
|
||||
SecretId: string;
|
||||
SecretString: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PutSecretValueHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
private readonly secretService: SecretService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Json;
|
||||
action = Action.SecretsManagerPutSecretValue;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
ClientRequestToken: Joi.string(),
|
||||
SecretId: Joi.string().required(),
|
||||
SecretString: Joi.string(),
|
||||
});
|
||||
|
||||
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const { SecretId, SecretString: secretString, ClientRequestToken } = params;
|
||||
const name = Secret.getNameFromSecretId(SecretId);
|
||||
const oldSecret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||
|
||||
if (!oldSecret) {
|
||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||
}
|
||||
|
||||
const secret = await this.secretService.create({
|
||||
versionId: ClientRequestToken,
|
||||
name: oldSecret.name,
|
||||
secretString,
|
||||
accountId: awsProperties.accountId,
|
||||
region: awsProperties.region,
|
||||
});
|
||||
|
||||
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name, VersionStages: [] }
|
||||
}
|
||||
}
|
||||
|
|
@ -28,4 +28,9 @@ export class Secret extends BaseEntity {
|
|||
get arn(): string {
|
||||
return `arn:aws:secretsmanager:${this.region}:${this.accountId}:${this.name}`;
|
||||
}
|
||||
|
||||
static getNameFromSecretId(secretId: string) {
|
||||
const parts = secretId.split(':');
|
||||
return parts.length > 1 ? parts.pop() : secretId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateSecretDto } from './create-secret.dto';
|
||||
import { Secret } from './secret.entity';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class SecretService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Secret)
|
||||
private readonly secretRepo: Repository<Secret>,
|
||||
) {}
|
||||
|
||||
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret> {
|
||||
return await this.secretRepo.findOne({ where: { name, region }, order: { createdAt: 'DESC' } });
|
||||
}
|
||||
|
||||
async findByNameAndVersion(name: string, versionId: string): Promise<Secret> {
|
||||
// TypeORM BUG: https://github.com/typeorm/typeorm/issues/5694 - Cannot use findOne here
|
||||
const [ secret ] = await this.secretRepo.find({ where: { name, versionId } });
|
||||
return secret;
|
||||
}
|
||||
|
||||
async create(dto: CreateSecretDto): Promise<Secret> {
|
||||
return await this.secretRepo.create({
|
||||
...dto,
|
||||
versionId: dto.versionId ?? uuid.v4(),
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,21 @@ import { DefaultActionHandlerProvider } from '../default-action-handler/default-
|
|||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||
import { CreateSecretHandler } from './create-secret.handler';
|
||||
import { DescribeSecretHandler } from './describe-secret.handler';
|
||||
import { GetResourcePolicyHandler } from './get-resource-policy.handler';
|
||||
import { GetSecretValueHandler } from './get-secret-value.handler';
|
||||
import { PutResourcePolicyHandler } from './put-resource-policy.handler';
|
||||
import { PutSecretValueHandler } from './put-secret-value.handler';
|
||||
import { Secret } from './secret.entity';
|
||||
import { SecretService } from './secret.service';
|
||||
import { SecretsManagerHandlers } from './secrets-manager.constants';
|
||||
|
||||
const handlers = [
|
||||
CreateSecretHandler,
|
||||
DescribeSecretHandler,
|
||||
GetResourcePolicyHandler,
|
||||
GetSecretValueHandler,
|
||||
PutResourcePolicyHandler,
|
||||
PutSecretValueHandler,
|
||||
]
|
||||
|
||||
const actions = [
|
||||
|
|
@ -46,6 +55,7 @@ const actions = [
|
|||
AwsSharedEntitiesModule,
|
||||
],
|
||||
providers: [
|
||||
SecretService,
|
||||
...handlers,
|
||||
ExistingActionHandlersProvider(handlers),
|
||||
DefaultActionHandlerProvider(SecretsManagerHandlers, Format.Json, actions),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { SqsQueueEntryService } from '../sqs/sqs-queue-entry.service';
|
||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||
import * as uuid from 'uuid';
|
||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||
import { SqsQueue } from '../sqs/sqs-queue.entity';
|
||||
|
||||
type QueryParams = {
|
||||
TopicArn: string;
|
||||
TargetArn: string;
|
||||
Subject?: string;
|
||||
Message: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PublishHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SnsTopicSubscription)
|
||||
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
|
||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||
private readonly attributeService: AttributesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SnsPublish;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
TargetArn: Joi.string(),
|
||||
TopicArn: Joi.string(),
|
||||
Subject: Joi.string().allow(null, ''),
|
||||
Message: Joi.string().required(),
|
||||
});
|
||||
|
||||
protected async handle({ TopicArn, TargetArn, Message, Subject }: QueryParams, awsProperties: AwsProperties) {
|
||||
const arn = TopicArn ?? TargetArn;
|
||||
|
||||
if (!arn) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
const MessageId = uuid.v4();
|
||||
const subscriptions = await this.snsTopicSubscriptionRepo.find({ where: { topicArn: arn } });
|
||||
const topicAttributes = await this.attributeService.getByArn(arn);
|
||||
|
||||
for (const sub of subscriptions) {
|
||||
const attributes = await this.attributeService.getByArn(sub.arn);
|
||||
if (sub.protocol === 'sqs') {
|
||||
const { value: isRaw } = attributes.find(a => a.name === 'RawMessageDelivery');
|
||||
const [queueAccountId, queueName] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(sub.endpoint);
|
||||
|
||||
const message = isRaw === 'true' ? Message : JSON.stringify({
|
||||
Type: "Notification",
|
||||
MessageId,
|
||||
TopicArn: arn,
|
||||
Subject,
|
||||
Message,
|
||||
Timestamp: new Date().toISOString(),
|
||||
SignatureVersion: topicAttributes.find(a => a.name === 'SignatureVersion')?.value ?? '1',
|
||||
Signature: '',
|
||||
SigningCertURL: '',
|
||||
UnsubscribeURL: `${awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${sub.arn}`,
|
||||
});
|
||||
|
||||
await this.sqsQueueEntryService.publish(queueAccountId, queueName, message);
|
||||
}
|
||||
}
|
||||
|
||||
return { MessageId };
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,13 @@ import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entit
|
|||
import { ExistingActionHandlers } from '../default-action-handler/default-action-handler.constants';
|
||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||
import { SqsModule } from '../sqs/sqs.module';
|
||||
import { CreateTopicHandler } from './create-topic.handler';
|
||||
import { GetSubscriptionAttributesHandler } from './get-subscription-attributes.handler';
|
||||
import { GetTopicAttributesHandler } from './get-topic-attributes.handler';
|
||||
import { ListTagsForResourceHandler } from './list-tags-for-resource.handler';
|
||||
import { ListTopicsHandler } from './list-topics.handler';
|
||||
import { PublishHandler } from './publish.handler';
|
||||
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
|
||||
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
|
||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||
|
|
@ -24,6 +26,7 @@ const handlers = [
|
|||
GetTopicAttributesHandler,
|
||||
ListTagsForResourceHandler,
|
||||
ListTopicsHandler,
|
||||
PublishHandler,
|
||||
SetSubscriptionAttributesHandler,
|
||||
SetTopicAttributesHandler,
|
||||
SubscribeHandler,
|
||||
|
|
@ -78,6 +81,7 @@ const actions = [
|
|||
imports: [
|
||||
TypeOrmModule.forFeature([SnsTopic, SnsTopicSubscription]),
|
||||
AwsSharedEntitiesModule,
|
||||
SqsModule,
|
||||
],
|
||||
providers: [
|
||||
...handlers,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||
|
||||
type QueryParams = {
|
||||
QueueName: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CreateQueueHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SqsQueue)
|
||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||
private readonly tagsService: TagsService,
|
||||
private readonly attributeService: AttributesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SqsCreateQueue;
|
||||
validator = Joi.object<QueryParams, true>({ QueueName: Joi.string().required() });
|
||||
|
||||
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const { QueueName: name } = params;
|
||||
|
||||
const queue = await this.sqsQueueRepo.create({
|
||||
name,
|
||||
accountId: awsProperties.accountId,
|
||||
region: awsProperties.region,
|
||||
}).save();
|
||||
|
||||
const tags = TagsService.tagPairs(params);
|
||||
await this.tagsService.createMany(queue.arn, tags);
|
||||
|
||||
const attributes = AttributesService.attributePairs(params);
|
||||
await this.attributeService.createMany(queue.arn, attributes);
|
||||
|
||||
return { QueueUrl: queue.getUrl(awsProperties.host) };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||
|
||||
type QueryParams = {
|
||||
__path: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PurgeQueueHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SqsPurgeQueue;
|
||||
validator = Joi.object<QueryParams, true>({ __path: Joi.string().required() });
|
||||
|
||||
protected async handle({ __path }: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(__path);
|
||||
await this.sqsQueueEntryService.purge(accountId, name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||
import crypto from 'crypto';
|
||||
|
||||
type QueryParams = {
|
||||
__path: string;
|
||||
MaxNumberOfMessages?: number;
|
||||
VisibilityTimeout?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReceiveMessageHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SqsReceiveMessage;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
__path: Joi.string().required(),
|
||||
MaxNumberOfMessages: Joi.number(),
|
||||
VisibilityTimeout: Joi.number(),
|
||||
});
|
||||
|
||||
protected async handle({ __path, MaxNumberOfMessages, VisibilityTimeout }: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(__path);
|
||||
const records = await this.sqsQueueEntryService.recieveMessages(accountId, name, MaxNumberOfMessages, VisibilityTimeout);
|
||||
return records.map(r => ({
|
||||
Message: {
|
||||
MessageId: r.id,
|
||||
ReceiptHandle: r.id,
|
||||
MD5OfBody: crypto.createHash('md5').update(r.message).digest("hex"),
|
||||
Body: r.message,
|
||||
'#': [
|
||||
{ Attribute: { Name: 'SenderId', Value: r.senderId }},
|
||||
{ Attribute: { Name: 'SentTimestamp', Value: r.createdAt.getSeconds() }},
|
||||
{ Attribute: { Name: 'ApproximateReceiveCount', Value: 1 }},
|
||||
{ Attribute: { Name: 'ApproximateFirstReceiveTimestamp', Value: r.createdAt.getSeconds() }},
|
||||
]
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
type QueryParams = {
|
||||
'Attribute.Name': string;
|
||||
'Attribute.Value': string;
|
||||
__path: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SetQueueAttributesHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SqsQueue)
|
||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||
private readonly attributeService: AttributesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SqsSetQueueAttributes;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
'Attribute.Name': Joi.string().required(),
|
||||
'Attribute.Value': Joi.string().required(),
|
||||
__path: Joi.string().required(),
|
||||
});
|
||||
|
||||
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(params.__path);
|
||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId , name } });
|
||||
|
||||
if(!queue) {
|
||||
throw new BadRequestException('ResourceNotFoundException');
|
||||
}
|
||||
|
||||
await this.attributeService.create({ name: params['Attribute.Name'], value: params['Attribute.Value'], arn: queue.arn });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
type QueueEntry = {
|
||||
id: string;
|
||||
queueArn: string;
|
||||
senderId: string;
|
||||
message: string;
|
||||
inFlightReleaseDate: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SqsQueueEntryService {
|
||||
|
||||
// Heavy use may require event-driven locking implementation
|
||||
private queues: Record<string, QueueEntry[]> = {};
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SqsQueue)
|
||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||
) {}
|
||||
|
||||
async publish(accountId: string, queueName: string, message: string) {
|
||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
||||
|
||||
if (!queue) {
|
||||
console.warn(`Warning bad subscription to ${queueName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.queues) {
|
||||
this.queues[queue.arn] = [];
|
||||
}
|
||||
|
||||
this.queues[queue.arn].push({
|
||||
id: uuid.v4(),
|
||||
queueArn: queue.arn,
|
||||
senderId: accountId,
|
||||
message,
|
||||
inFlightReleaseDate: new Date(),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
async recieveMessages(accountId: string, queueName: string, maxNumberOfMessages = 10, visabilityTimeout = 0): Promise<QueueEntry[]> {
|
||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
||||
|
||||
if (!queue) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
const accessDate = new Date();
|
||||
const newInFlightReleaseDate = new Date(accessDate);
|
||||
newInFlightReleaseDate.setSeconds(accessDate.getSeconds() + visabilityTimeout);
|
||||
const records = this.queues[queue.arn]?.filter(e => e.inFlightReleaseDate <= accessDate).slice(0, maxNumberOfMessages - 1);
|
||||
records.forEach(e => e.inFlightReleaseDate = newInFlightReleaseDate);
|
||||
return records;
|
||||
}
|
||||
|
||||
async purge(accountId: string, queueName: string) {
|
||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
||||
this.queues[queue.arn] = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('sqs_queue')
|
||||
export class SqsQueue extends BaseEntity {
|
||||
|
||||
@PrimaryColumn({ name: 'name' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'account_id', nullable: false })
|
||||
accountId: string;
|
||||
|
||||
@Column({ name: 'region', nullable: false })
|
||||
region: string;
|
||||
|
||||
get arn(): string {
|
||||
return `arn:aws:sns:${this.region}:${this.accountId}:${this.name}`;
|
||||
}
|
||||
|
||||
getUrl(host: string): string {
|
||||
return `${host}/${this.accountId}/${this.name}`;
|
||||
}
|
||||
|
||||
static getAccountIdAndNameFromPath(path: string): [string, string] {
|
||||
const [_, accountId, name] = path.split('/');
|
||||
return [accountId, name];
|
||||
}
|
||||
|
||||
static getAccountIdAndNameFromArn(arn: string): [string, string] {
|
||||
const parts = arn.split(':');
|
||||
const name = parts.pop();
|
||||
const accountId = parts.pop();
|
||||
return [accountId, name];
|
||||
}
|
||||
|
||||
static tryGetAccountIdAndNameFromPathOrArn(pathOrArn: string): [string, string] {
|
||||
if (pathOrArn.split(':').length) {
|
||||
return SqsQueue.getAccountIdAndNameFromArn(pathOrArn);
|
||||
}
|
||||
return SqsQueue.getAccountIdAndNameFromPath(pathOrArn);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
|
||||
export type SqsHandlers = Record<Action, AbstractActionHandler>;
|
||||
export const SqsHandlers = Symbol.for('SQS_HANDLERS');
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||
import { CreateQueueHandler } from './create-queue.handler';
|
||||
import { PurgeQueueHandler } from './purge-queue.handler';
|
||||
import { SetQueueAttributesHandler } from './set-queue-attributes.handler';
|
||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
import { SqsHandlers } from './sqs.constants';
|
||||
|
||||
const handlers = [
|
||||
CreateQueueHandler,
|
||||
PurgeQueueHandler,
|
||||
SetQueueAttributesHandler,
|
||||
]
|
||||
|
||||
const actions = [
|
||||
Action.SqsAddPermisson,
|
||||
Action.SqsChangeMessageVisibility,
|
||||
Action.SqsChangeMessageVisibilityBatch,
|
||||
Action.SqsCreateQueue,
|
||||
Action.SqsDeleteMessage,
|
||||
Action.SqsDeleteMessageBatch,
|
||||
Action.SqsDeleteQueue,
|
||||
Action.SqsGetQueueAttributes,
|
||||
Action.SqsGetQueueUrl,
|
||||
Action.SqsListDeadLetterSourceQueues,
|
||||
Action.SqsListQueues,
|
||||
Action.SqsListQueueTags,
|
||||
Action.SqsPurgeQueue,
|
||||
Action.SqsReceiveMessage,
|
||||
Action.SqsRemovePermission,
|
||||
Action.SqsSendMessage,
|
||||
Action.SqsSendMessageBatch,
|
||||
Action.SqsSetQueueAttributes,
|
||||
Action.SqsTagQueue,
|
||||
Action.SqsUntagQueue,
|
||||
]
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([SqsQueue]),
|
||||
AwsSharedEntitiesModule,
|
||||
],
|
||||
providers: [
|
||||
...handlers,
|
||||
SqsQueueEntryService,
|
||||
ExistingActionHandlersProvider(handlers),
|
||||
DefaultActionHandlerProvider(SqsHandlers, Format.Xml, actions),
|
||||
],
|
||||
exports: [
|
||||
SqsHandlers,
|
||||
SqsQueueEntryService,
|
||||
]
|
||||
})
|
||||
export class SqsModule {}
|
||||
57
yarn.lock
57
yarn.lock
|
|
@ -301,6 +301,21 @@
|
|||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
||||
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/eslint-scope@^3.7.3":
|
||||
version "3.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
||||
|
|
@ -327,11 +342,35 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
||||
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
||||
|
||||
"@types/express-serve-static-core@^4.17.33":
|
||||
version "4.17.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543"
|
||||
integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
|
||||
"@types/express@^4.17.17":
|
||||
version "4.17.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
|
||||
integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.33"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.8":
|
||||
version "7.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/mime@*":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
|
||||
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
|
||||
|
||||
"@types/node@*":
|
||||
version "18.15.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
|
||||
|
|
@ -342,6 +381,24 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d"
|
||||
integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==
|
||||
dependencies:
|
||||
"@types/mime" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/uuid@8.3.4":
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||
|
|
|
|||
Loading…
Reference in New Issue