Compare commits

...

11 Commits

Author SHA1 Message Date
standardci
3d284461f3 chore(release): publish new version
- @standardnotes/api-gateway@1.6.29
 - @standardnotes/auth-server@1.11.30
 - @standardnotes/domain-events-infra@1.7.27
 - @standardnotes/domain-events@2.50.2
 - @standardnotes/event-store@1.1.26
 - @standardnotes/files-server@1.5.27
 - @standardnotes/predicates@1.3.0
 - @standardnotes/scheduler-server@1.8.2
 - @standardnotes/syncing-server@1.6.29
2022-07-25 18:22:30 +00:00
Karol Sójko
6642641c11 feat(predicates): add could-not-be-determined predicate verification result 2022-07-25 20:20:35 +02:00
standardci
3e637a482e chore(release): publish new version
- @standardnotes/scheduler-server@1.8.1
2022-07-25 11:02:23 +00:00
Karol Sójko
6374248132 fix(scheduler): checking for predicates fullfillment on applying discount 2022-07-25 13:00:46 +02:00
standardci
b9661d74ee chore(release): publish new version
- @standardnotes/scheduler-server@1.8.0
2022-07-25 07:45:53 +00:00
Karol Sójko
0a5b956cb9 feat(scheduler): add feature flag behind applying and withdrawing discounts 2022-07-25 09:44:06 +02:00
standardci
be88fd941d chore(release): publish new version
- @standardnotes/scheduler-server@1.7.0
2022-07-25 05:32:13 +00:00
Karol Sójko
48af9e7c1c feat(scheduler): add publishing discount apply/withdraw events 2022-07-25 07:30:32 +02:00
Karol Sójko
71684350e9 feat(scheduler): add creating discount apply/withdraw events 2022-07-25 07:20:50 +02:00
standardci
9a1924b7c6 chore(release): publish new version
- @standardnotes/scheduler-server@1.6.0
2022-07-22 11:38:37 +00:00
Karol Sójko
fc20697d81 feat(scheduler): schedule apply and withdraw subscription discounts upon registration 2022-07-22 13:37:00 +02:00
26 changed files with 330 additions and 11 deletions

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.29](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.6.28...@standardnotes/api-gateway@1.6.29) (2022-07-25)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.6.28](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.6.27...@standardnotes/api-gateway@1.6.28) (2022-07-15)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.6.28",
"version": "1.6.29",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.30](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.11.29...@standardnotes/auth-server@1.11.30) (2022-07-25)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.11.29](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.11.28...@standardnotes/auth-server@1.11.29) (2022-07-15)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.11.29",
"version": "1.11.30",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.7.27](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.7.26...@standardnotes/domain-events-infra@1.7.27) (2022-07-25)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.7.26](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.7.25...@standardnotes/domain-events-infra@1.7.26) (2022-07-15)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.7.26",
"version": "1.7.27",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.50.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.50.1...@standardnotes/domain-events@2.50.2) (2022-07-25)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.50.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.50.0...@standardnotes/domain-events@2.50.1) (2022-07-15)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.50.1",
"version": "2.50.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.26](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.1.25...@standardnotes/event-store@1.1.26) (2022-07-25)
**Note:** Version bump only for package @standardnotes/event-store
## [1.1.25](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.1.24...@standardnotes/event-store@1.1.25) (2022-07-15)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.1.25",
"version": "1.1.26",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.5.27](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.26...@standardnotes/files-server@1.5.27) (2022-07-25)
**Note:** Version bump only for package @standardnotes/files-server
## [1.5.26](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.25...@standardnotes/files-server@1.5.26) (2022-07-15)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.5.26",
"version": "1.5.27",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.3.0](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.2.6...@standardnotes/predicates@1.3.0) (2022-07-25)
### Features
* **predicates:** add could-not-be-determined predicate verification result ([6642641](https://github.com/standardnotes/server/commit/6642641c1161986a1c1186698f6b8151ce3aee87))
## [1.2.6](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.2.5...@standardnotes/predicates@1.2.6) (2022-07-14)
**Note:** Version bump only for package @standardnotes/predicates

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/predicates",
"version": "1.2.6",
"version": "1.3.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -1,4 +1,5 @@
export enum PredicateVerificationResult {
Affirmed = 'affirmed',
Denied = 'denied',
CouldNotBeDetermined = 'could-not-be-determined',
}

View File

@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.2](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.8.1...@standardnotes/scheduler-server@1.8.2) (2022-07-25)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.8.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.8.0...@standardnotes/scheduler-server@1.8.1) (2022-07-25)
### Bug Fixes
* **scheduler:** checking for predicates fullfillment on applying discount ([6374248](https://github.com/standardnotes/server/commit/637424813278b7dd81969e1783cbc38d1a916cab))
# [1.8.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.7.0...@standardnotes/scheduler-server@1.8.0) (2022-07-25)
### Features
* **scheduler:** add feature flag behind applying and withdrawing discounts ([0a5b956](https://github.com/standardnotes/server/commit/0a5b956cb9586d353ac68c79e8473d74f8d9796a))
# [1.7.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.6.0...@standardnotes/scheduler-server@1.7.0) (2022-07-25)
### Features
* **scheduler:** add creating discount apply/withdraw events ([7168435](https://github.com/standardnotes/server/commit/71684350e94053d884ae907e5d3deba4bc027f1b))
* **scheduler:** add publishing discount apply/withdraw events ([48af9e7](https://github.com/standardnotes/server/commit/48af9e7c1cfb582389af83e15977b930bf067f8d))
# [1.6.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.5.23...@standardnotes/scheduler-server@1.6.0) (2022-07-22)
### Features
* **scheduler:** schedule apply and withdraw subscription discounts upon registration ([fc20697](https://github.com/standardnotes/server/commit/fc20697d81419827c4f51c5c80804dd98804f33f))
## [1.5.23](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.5.22...@standardnotes/scheduler-server@1.5.23) (2022-07-15)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.5.23",
"version": "1.8.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -19,6 +19,52 @@ describe('DomainEventFactory', () => {
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
})
it('should create a DISCOUNT_APPLY_REQUESTED event', () => {
expect(
createFactory().createDiscountApplyRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'off-10',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'off-10',
},
type: 'DISCOUNT_APPLY_REQUESTED',
})
})
it('should create a DISCOUNT_WITHDRAW_REQUESTED event', () => {
expect(
createFactory().createDiscountWithdrawRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'off-10',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'off-10',
},
type: 'DISCOUNT_WITHDRAW_REQUESTED',
})
})
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
expect(
createFactory().createEmailMessageRequestedEvent({

View File

@@ -1,5 +1,7 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
DomainEventService,
EmailMessageRequestedEvent,
PredicateVerificationRequestedEvent,
@@ -16,6 +18,39 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent {
return {
type: 'DISCOUNT_APPLY_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userEmail,
userIdentifierType: 'email',
},
origin: DomainEventService.Scheduler,
},
payload: dto,
}
}
createDiscountWithdrawRequestedEvent(dto: {
userEmail: string
discountCode: string
}): DiscountWithdrawRequestedEvent {
return {
type: 'DISCOUNT_WITHDRAW_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userEmail,
userIdentifierType: 'email',
},
origin: DomainEventService.Scheduler,
},
payload: dto,
}
}
createEmailMessageRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier

View File

@@ -1,5 +1,10 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import { EmailMessageRequestedEvent, PredicateVerificationRequestedEvent } from '@standardnotes/domain-events'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
EmailMessageRequestedEvent,
PredicateVerificationRequestedEvent,
} from '@standardnotes/domain-events'
import { Job } from '../Job/Job'
import { Predicate } from '../Predicate/Predicate'
@@ -11,4 +16,6 @@ export interface DomainEventFactoryInterface {
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent
createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent
createDiscountWithdrawRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountWithdrawRequestedEvent
}

View File

@@ -24,6 +24,10 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
await this.scheduleEncourageEmailBackupsJob(event)
await this.scheduleEncourageSubscriptionPurchasing(event)
await this.scheduleSubscriptionDiscountApplying(event)
await this.scheduleSubscriptionDiscountWithdraw(event)
}
private async scheduleEncourageEmailBackupsJob(event: UserRegisteredEvent): Promise<void> {
@@ -65,4 +69,36 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
await this.predicateRepository.save(predicate)
}
private async scheduleSubscriptionDiscountApplying(event: UserRegisteredEvent): Promise<void> {
const job = new Job()
job.name = JobName.APPLY_SUBSCRIPTION_DISCOUNT
job.scheduledAt = this.timer.convertDateToMicroseconds(this.timer.getUTCDateNDaysAhead(7))
job.createdAt = this.timer.getTimestampInMicroseconds()
job.status = JobStatus.Pending
job.userIdentifier = event.payload.email
job.userIdentifierType = 'email'
await this.jobRepository.save(job)
const predicate = new Predicate()
predicate.name = PredicateName.SubscriptionPurchased
predicate.status = PredicateStatus.Pending
predicate.authority = PredicateAuthority.Auth
predicate.job = Promise.resolve(job)
await this.predicateRepository.save(predicate)
}
private async scheduleSubscriptionDiscountWithdraw(event: UserRegisteredEvent): Promise<void> {
const job = new Job()
job.name = JobName.WITHDRAW_SUBSCRIPTION_DISCOUNT
job.scheduledAt = this.timer.convertDateToMicroseconds(this.timer.getUTCDateNDaysAhead(12))
job.createdAt = this.timer.getTimestampInMicroseconds()
job.status = JobStatus.Pending
job.userIdentifier = event.payload.email
job.userIdentifierType = 'email'
await this.jobRepository.save(job)
}
}

View File

@@ -1,4 +1,9 @@
import { DomainEventPublisherInterface, EmailMessageRequestedEvent } from '@standardnotes/domain-events'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
DomainEventPublisherInterface,
EmailMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { PredicateName } from '@standardnotes/predicates'
import 'reflect-metadata'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
@@ -35,6 +40,12 @@ describe('JobDoneInterpreter', () => {
domainEventFactory.createEmailMessageRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<EmailMessageRequestedEvent>)
domainEventFactory.createDiscountApplyRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<DiscountApplyRequestedEvent>)
domainEventFactory.createDiscountWithdrawRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<DiscountWithdrawRequestedEvent>)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
@@ -172,6 +183,95 @@ describe('JobDoneInterpreter', () => {
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should request discount apply', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: JobName.APPLY_SUBSCRIPTION_DISCOUNT,
userIdentifier: 'test@standardnotes.com',
userIdentifierType: 'email',
} as jest.Mocked<Job>)
predicateRepository.findByJobUuid = jest
.fn()
.mockReturnValue([
{ name: PredicateName.SubscriptionPurchased, status: PredicateStatus.Denied } as jest.Mocked<Predicate>,
])
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createDiscountApplyRequestedEvent).toHaveBeenCalledWith({
userEmail: 'test@standardnotes.com',
discountCode: 'econ-10',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should not request discount apply if email is not internal', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: JobName.APPLY_SUBSCRIPTION_DISCOUNT,
userIdentifier: 'test@test.com',
userIdentifierType: 'email',
} as jest.Mocked<Job>)
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createDiscountApplyRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not request discount apply if email is missing', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: JobName.APPLY_SUBSCRIPTION_DISCOUNT,
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>)
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createDiscountApplyRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should request discount withdraw', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: JobName.WITHDRAW_SUBSCRIPTION_DISCOUNT,
userIdentifier: 'test@standardnotes.com',
userIdentifierType: 'email',
} as jest.Mocked<Job>)
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createDiscountWithdrawRequestedEvent).toHaveBeenCalledWith({
userEmail: 'test@standardnotes.com',
discountCode: 'econ-10',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should not request discount withdraw if email is not internal', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: JobName.WITHDRAW_SUBSCRIPTION_DISCOUNT,
userIdentifier: 'test@test.com',
userIdentifierType: 'email',
} as jest.Mocked<Job>)
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createDiscountWithdrawRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not request discount withdraw if email is missing', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: JobName.WITHDRAW_SUBSCRIPTION_DISCOUNT,
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>)
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createDiscountWithdrawRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should do nothing if there is no interpretation for a given job', async () => {
jobRepository.findOneByUuid = jest.fn().mockReturnValue({
name: 'foobar' as JobName,

View File

@@ -49,6 +49,16 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
await this.requestExitInterviewEmail(job.userIdentifier)
}
return
case JobName.APPLY_SUBSCRIPTION_DISCOUNT:
if (job.userIdentifierType === 'email' && job.userIdentifier.includes('@standardnotes.com')) {
await this.requestDiscountApply(job.userIdentifier)
}
return
case JobName.WITHDRAW_SUBSCRIPTION_DISCOUNT:
if (job.userIdentifierType === 'email' && job.userIdentifier.includes('@standardnotes.com')) {
await this.requestDiscountWithdraw(job.userIdentifier)
}
return
default:
return
}
@@ -86,6 +96,24 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
)
}
private async requestDiscountApply(userEmail: string): Promise<void> {
await this.domainEventPublisher.publish(
this.domainEventFactory.createDiscountApplyRequestedEvent({
userEmail,
discountCode: 'econ-10',
}),
)
}
private async requestDiscountWithdraw(userEmail: string): Promise<void> {
await this.domainEventPublisher.publish(
this.domainEventFactory.createDiscountWithdrawRequestedEvent({
userEmail,
discountCode: 'econ-10',
}),
)
}
private async predicatesAreFulfilled(job: Job): Promise<boolean> {
const predicates = await this.predicateRepository.findByJobUuid(job.uuid)
@@ -96,6 +124,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
PredicateStatus.Denied
)
case JobName.ENCOURAGE_SUBSCRIPTION_PURCHASING:
case JobName.APPLY_SUBSCRIPTION_DISCOUNT:
return (
predicates.find((predicate) => predicate.name === PredicateName.SubscriptionPurchased)?.status ===
PredicateStatus.Denied

View File

@@ -3,4 +3,6 @@ export enum JobName {
ENCOURAGE_EMAIL_BACKUPS = 'encourage-email-backups',
ENCOURAGE_SUBSCRIPTION_PURCHASING = 'encourage-subscription-purchasing',
EXIT_INTERVIEW = 'exit-interview',
APPLY_SUBSCRIPTION_DISCOUNT = 'apply-subscription-discount',
WITHDRAW_SUBSCRIPTION_DISCOUNT = 'withdraw-subscription-discount',
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.29](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.28...@standardnotes/syncing-server@1.6.29) (2022-07-25)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.28](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.27...@standardnotes/syncing-server@1.6.28) (2022-07-15)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.6.28",
"version": "1.6.29",
"engines": {
"node": ">=16.0.0 <17.0.0"
},