feat(api): add subscription server and client services and interfaces (#1470)

* feat(api): add subscription server and client services and interfaces

* fix(api): linter issues

* feat(models): add subscription invitations

* feat(api): add subscriptions invitation operations on server side

* fix(api): linter issues
This commit is contained in:
Karol Sójko
2022-08-31 16:08:52 +02:00
committed by GitHub
parent 370ce39eba
commit 089d3a2e66
37 changed files with 533 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -22,12 +22,14 @@
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"test": "jest spec --coverage"
},
"devDependencies": {
"@types/jest": "^28.1.5",
"@types/lodash": "^4.14.182",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"eslint": "^8.23.0",
"eslint-plugin-prettier": "*",
"jest": "^28.1.2",
"ts-jest": "^28.0.5"

View File

@@ -0,0 +1,63 @@
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface'
import { SubscriptionApiService } from './SubscriptionApiService'
describe('SubscriptionApiService', () => {
let subscriptionServer: SubscriptionServerInterface
const createService = () => new SubscriptionApiService(subscriptionServer)
beforeEach(() => {
subscriptionServer = {} as jest.Mocked<SubscriptionServerInterface>
subscriptionServer.invite = jest.fn().mockReturnValue({
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
} as jest.Mocked<SubscriptionInviteResponse>)
})
it('should invite a user', async () => {
const response = await createService().invite('test@test.te')
expect(response).toEqual({
data: {
success: true,
sharedSubscriptionInvitationUuid: '1-2-3',
},
})
expect(subscriptionServer.invite).toHaveBeenCalledWith({
api: '20200115',
identifier: 'test@test.te',
})
})
it('should not invite a user if it is already inviting', async () => {
const service = createService()
Object.defineProperty(service, 'inviting', {
get: () => true,
})
let error = null
try {
await service.invite('test@test.te')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not invite a user if the server fails', async () => {
subscriptionServer.invite = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().invite('test@test.te')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})

View File

@@ -0,0 +1,35 @@
import { ErrorMessage } from '../../Error/ErrorMessage'
import { ApiCallError } from '../../Error/ApiCallError'
import { ApiVersion } from '../../Api/ApiVersion'
import { ApiEndpointParam } from '../../Request/ApiEndpointParam'
import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface'
import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface'
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
export class SubscriptionApiService implements SubscriptionApiServiceInterface {
private inviting: boolean
constructor(private subscriptionServer: SubscriptionServerInterface) {
this.inviting = false
}
async invite(inviteeEmail: string): Promise<SubscriptionInviteResponse> {
if (this.inviting) {
throw new ApiCallError(ErrorMessage.InvitingInProgress)
}
this.inviting = true
try {
const response = await this.subscriptionServer.invite({
[ApiEndpointParam.ApiVersion]: ApiVersion.v0,
identifier: inviteeEmail,
})
this.inviting = false
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
}
}
}

View File

@@ -0,0 +1,5 @@
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
export interface SubscriptionApiServiceInterface {
invite(inviteeEmail: string): Promise<SubscriptionInviteResponse>
}

View File

@@ -1,2 +1,4 @@
export * from './Subscription/SubscriptionApiService'
export * from './Subscription/SubscriptionApiServiceInterface'
export * from './User/UserApiService'
export * from './User/UserApiServiceInterface'

View File

@@ -1,7 +1,9 @@
export enum ErrorMessage {
InvitingInProgress = 'An existing invitation request is already in progress.',
RegistrationInProgress = 'An existing registration request is already in progress.',
GenericRegistrationFail = 'A server error occurred while trying to register. Please try again.',
RateLimited = 'Too many successive server requests. Please wait a few minutes and try again.',
InsufficientPasswordMessage = 'Your password must be at least %LENGTH% characters in length. For your security, please choose a longer password or, ideally, a passphrase, and try again.',
PasscodeRequired = 'Your passcode is required in order to register for an account.',
GenericFail = 'A server error occurred. Please try again.',
}

View File

@@ -0,0 +1,10 @@
import { Uuid } from '@standardnotes/common'
import { ApiEndpointParam } from '../ApiEndpointParam'
import { ApiVersion } from '../../Api/ApiVersion'
export type SubscriptionInviteAcceptRequestParams = {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
inviteUuid: Uuid
[additionalParam: string]: unknown
}

View File

@@ -0,0 +1,10 @@
import { Uuid } from '@standardnotes/common'
import { ApiEndpointParam } from '../ApiEndpointParam'
import { ApiVersion } from '../../Api/ApiVersion'
export type SubscriptionInviteCancelRequestParams = {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
inviteUuid: Uuid
[additionalParam: string]: unknown
}

View File

@@ -0,0 +1,10 @@
import { Uuid } from '@standardnotes/common'
import { ApiEndpointParam } from '../ApiEndpointParam'
import { ApiVersion } from '../../Api/ApiVersion'
export type SubscriptionInviteDeclineRequestParams = {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
inviteUuid: Uuid
[additionalParam: string]: unknown
}

View File

@@ -0,0 +1,7 @@
import { ApiEndpointParam } from '../ApiEndpointParam'
import { ApiVersion } from '../../Api/ApiVersion'
export type SubscriptionInviteListRequestParams = {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
[additionalParam: string]: unknown
}

View File

@@ -0,0 +1,8 @@
import { ApiEndpointParam } from '../ApiEndpointParam'
import { ApiVersion } from '../../Api/ApiVersion'
export type SubscriptionInviteRequestParams = {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
identifier: string
[additionalParam: string]: unknown
}

View File

@@ -1,2 +1,7 @@
export * from './ApiEndpointParam'
export * from './Subscription/SubscriptionInviteAcceptRequestParams'
export * from './Subscription/SubscriptionInviteCancelRequestParams'
export * from './Subscription/SubscriptionInviteDeclineRequestParams'
export * from './Subscription/SubscriptionInviteListRequestParams'
export * from './Subscription/SubscriptionInviteRequestParams'
export * from './User/UserRegistrationRequestParams'

View File

@@ -0,0 +1,7 @@
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteAcceptResponseBody } from './SubscriptionInviteAcceptResponseBody'
export interface SubscriptionInviteAcceptResponse extends HttpResponse {
data: SubscriptionInviteAcceptResponseBody | HttpErrorResponseBody
}

View File

@@ -0,0 +1,3 @@
export type SubscriptionInviteAcceptResponseBody = {
success: boolean
}

View File

@@ -0,0 +1,7 @@
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteCancelResponseBody } from './SubscriptionInviteCancelResponseBody'
export interface SubscriptionInviteCancelResponse extends HttpResponse {
data: SubscriptionInviteCancelResponseBody | HttpErrorResponseBody
}

View File

@@ -0,0 +1,3 @@
export type SubscriptionInviteCancelResponseBody = {
success: boolean
}

View File

@@ -0,0 +1,7 @@
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteDeclineResponseBody } from './SubscriptionInviteDeclineResponseBody'
export interface SubscriptionInviteDeclineResponse extends HttpResponse {
data: SubscriptionInviteDeclineResponseBody | HttpErrorResponseBody
}

View File

@@ -0,0 +1,3 @@
export type SubscriptionInviteDeclineResponseBody = {
success: boolean
}

View File

@@ -0,0 +1,7 @@
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteListResponseBody } from './SubscriptionInviteListResponseBody'
export interface SubscriptionInviteListResponse extends HttpResponse {
data: SubscriptionInviteListResponseBody | HttpErrorResponseBody
}

View File

@@ -0,0 +1,5 @@
import { Invitation } from '@standardnotes/models'
export type SubscriptionInviteListResponseBody = {
invitations: Array<Invitation>
}

View File

@@ -0,0 +1,7 @@
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SubscriptionInviteResponseBody } from './SubscriptionInviteResponseBody'
export interface SubscriptionInviteResponse extends HttpResponse {
data: SubscriptionInviteResponseBody | HttpErrorResponseBody
}

View File

@@ -0,0 +1,10 @@
import { Uuid } from '@standardnotes/common'
export type SubscriptionInviteResponseBody =
| {
success: true
sharedSubscriptionInvitationUuid: Uuid
}
| {
success: false
}

View File

@@ -1,2 +1,12 @@
export * from './Subscription/SubscriptionInviteAcceptResponse'
export * from './Subscription/SubscriptionInviteAcceptResponseBody'
export * from './Subscription/SubscriptionInviteCancelResponse'
export * from './Subscription/SubscriptionInviteCancelResponseBody'
export * from './Subscription/SubscriptionInviteDeclineResponse'
export * from './Subscription/SubscriptionInviteDeclineResponseBody'
export * from './Subscription/SubscriptionInviteListResponse'
export * from './Subscription/SubscriptionInviteListResponseBody'
export * from './Subscription/SubscriptionInviteResponse'
export * from './Subscription/SubscriptionInviteResponseBody'
export * from './User/UserRegistrationResponse'
export * from './User/UserRegistrationResponseBody'

View File

@@ -0,0 +1,15 @@
import { Uuid } from '@standardnotes/common'
const SharingPaths = {
invite: '/v1/subscription-invites',
acceptInvite: (inviteUuid: Uuid) => `/v1/subscription-invites/${inviteUuid}/accept`,
declineInvite: (inviteUuid: Uuid) => `/v1/subscription-invites/${inviteUuid}/decline`,
cancelInvite: (inviteUuid: Uuid) => `/v1/subscription-invites/${inviteUuid}`,
listInvites: '/v1/subscription-invites',
}
export const Paths = {
v1: {
...SharingPaths,
},
}

View File

@@ -0,0 +1,105 @@
import { Invitation } from '@standardnotes/models'
import { ApiVersion } from '../../Api'
import { HttpServiceInterface } from '../../Http'
import { SubscriptionInviteResponse } from '../../Response'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionServer } from './SubscriptionServer'
describe('SubscriptionServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new SubscriptionServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
})
it('should invite a user to a shared subscription', async () => {
httpService.post = jest.fn().mockReturnValue({
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
} as jest.Mocked<SubscriptionInviteResponse>)
const response = await createServer().invite({
api: ApiVersion.v0,
identifier: 'test@test.te',
})
expect(response).toEqual({
data: {
success: true,
sharedSubscriptionInvitationUuid: '1-2-3',
},
})
})
it('should accept an invite to a shared subscription', async () => {
httpService.get = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
const response = await createServer().acceptInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should decline an invite to a shared subscription', async () => {
httpService.get = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteDeclineResponse>)
const response = await createServer().declineInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should cancel an invite to a shared subscription', async () => {
httpService.delete = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteCancelResponse>)
const response = await createServer().cancelInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should list invitations to a shared subscription', async () => {
httpService.get = jest.fn().mockReturnValue({
data: { invitations: [{} as jest.Mocked<Invitation>] },
} as jest.Mocked<SubscriptionInviteListResponse>)
const response = await createServer().listInvites({
api: ApiVersion.v0,
})
expect(response).toEqual({
data: {
invitations: [{} as jest.Mocked<Invitation>],
},
})
})
})

View File

@@ -0,0 +1,48 @@
import { HttpServiceInterface } from '../../Http/HttpServiceInterface'
import { SubscriptionInviteAcceptRequestParams } from '../../Request/Subscription/SubscriptionInviteAcceptRequestParams'
import { SubscriptionInviteCancelRequestParams } from '../../Request/Subscription/SubscriptionInviteCancelRequestParams'
import { SubscriptionInviteDeclineRequestParams } from '../../Request/Subscription/SubscriptionInviteDeclineRequestParams'
import { SubscriptionInviteListRequestParams } from '../../Request/Subscription/SubscriptionInviteListRequestParams'
import { SubscriptionInviteRequestParams } from '../../Request/Subscription/SubscriptionInviteRequestParams'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
import { Paths } from './Paths'
import { SubscriptionServerInterface } from './SubscriptionServerInterface'
export class SubscriptionServer implements SubscriptionServerInterface {
constructor(private httpService: HttpServiceInterface) {}
async acceptInvite(params: SubscriptionInviteAcceptRequestParams): Promise<SubscriptionInviteAcceptResponse> {
const response = await this.httpService.get(Paths.v1.acceptInvite(params.inviteUuid), params)
return response as SubscriptionInviteAcceptResponse
}
async declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse> {
const response = await this.httpService.get(Paths.v1.declineInvite(params.inviteUuid), params)
return response as SubscriptionInviteDeclineResponse
}
async cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse> {
const response = await this.httpService.delete(Paths.v1.cancelInvite(params.inviteUuid), params)
return response as SubscriptionInviteCancelResponse
}
async listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse> {
const response = await this.httpService.get(Paths.v1.listInvites, params)
return response as SubscriptionInviteListResponse
}
async invite(params: SubscriptionInviteRequestParams): Promise<SubscriptionInviteResponse> {
const response = await this.httpService.post(Paths.v1.invite, params)
return response as SubscriptionInviteResponse
}
}

View File

@@ -0,0 +1,18 @@
import { SubscriptionInviteAcceptRequestParams } from '../../Request/Subscription/SubscriptionInviteAcceptRequestParams'
import { SubscriptionInviteCancelRequestParams } from '../../Request/Subscription/SubscriptionInviteCancelRequestParams'
import { SubscriptionInviteDeclineRequestParams } from '../../Request/Subscription/SubscriptionInviteDeclineRequestParams'
import { SubscriptionInviteListRequestParams } from '../../Request/Subscription/SubscriptionInviteListRequestParams'
import { SubscriptionInviteRequestParams } from '../../Request/Subscription/SubscriptionInviteRequestParams'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
export interface SubscriptionServerInterface {
invite(params: SubscriptionInviteRequestParams): Promise<SubscriptionInviteResponse>
acceptInvite(params: SubscriptionInviteAcceptRequestParams): Promise<SubscriptionInviteAcceptResponse>
declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse>
cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse>
listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse>
}

View File

@@ -0,0 +1,15 @@
import { InvitationStatus } from './InvitationStatus'
import { InviteeIdentifierType } from './InviteeIdentifierType'
import { InviterIdentifierType } from './InviterIdentifierType'
export type Invitation = {
uuid: string
inviterIdentifier: string
inviterIdentifierType: InviterIdentifierType
inviteeIdentifier: string
inviteeIdentifierType: InviteeIdentifierType
status: InvitationStatus
subscriptionId: number
createdAt: number
updatedAt: number
}

View File

@@ -0,0 +1,6 @@
export enum InvitationStatus {
Sent = 'sent',
Canceled = 'canceled',
Accepted = 'accepted',
Declined = 'declined',
}

View File

@@ -0,0 +1,5 @@
export enum InviteeIdentifierType {
Email = 'email',
Hash = 'hash',
Uuid = 'uuid',
}

View File

@@ -0,0 +1,4 @@
export enum InviterIdentifierType {
Email = 'email',
Uuid = 'uuid',
}

View File

@@ -24,6 +24,10 @@ export * from './Abstract/Contextual/SessionHistory'
export * from './Abstract/Item'
export * from './Abstract/Payload'
export * from './Abstract/TransferPayload'
export * from './Api/Subscription/Invitation'
export * from './Api/Subscription/InvitationStatus'
export * from './Api/Subscription/InviteeIdentifierType'
export * from './Api/Subscription/InviterIdentifierType'
export * from './Local/KeyParams/RootKeyParamsInterface'
export * from './Local/RootKey/KeychainTypes'
export * from './Local/RootKey/RootKeyContent'

View File

@@ -3463,6 +3463,23 @@ __metadata:
languageName: node
linkType: hard
"@eslint/eslintrc@npm:^1.3.1":
version: 1.3.1
resolution: "@eslint/eslintrc@npm:1.3.1"
dependencies:
ajv: ^6.12.4
debug: ^4.3.2
espree: ^9.4.0
globals: ^13.15.0
ignore: ^5.2.0
import-fresh: ^3.2.1
js-yaml: ^4.1.0
minimatch: ^3.1.2
strip-json-comments: ^3.1.1
checksum: 9844dcc58a44399649926d5a17a2d53d529b80d3e8c3e9d0964ae198bac77ee6bb1cf44940f30cd9c2e300f7568ec82500be42ace6cacefb08aebf9905fe208e
languageName: node
linkType: hard
"@expo/react-native-action-sheet@npm:^3.13.0":
version: 3.13.0
resolution: "@expo/react-native-action-sheet@npm:3.13.0"
@@ -3538,6 +3555,13 @@ __metadata:
languageName: node
linkType: hard
"@humanwhocodes/module-importer@npm:^1.0.1":
version: 1.0.1
resolution: "@humanwhocodes/module-importer@npm:1.0.1"
checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61
languageName: node
linkType: hard
"@humanwhocodes/object-schema@npm:^1.2.0, @humanwhocodes/object-schema@npm:^1.2.1":
version: 1.2.1
resolution: "@humanwhocodes/object-schema@npm:1.2.1"
@@ -6216,6 +6240,7 @@ __metadata:
"@types/jest": ^28.1.5
"@types/lodash": ^4.14.182
"@typescript-eslint/eslint-plugin": ^5.30.0
eslint: ^8.23.0
eslint-plugin-prettier: "*"
jest: ^28.1.2
reflect-metadata: ^0.1.13
@@ -18026,6 +18051,55 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:^8.23.0":
version: 8.23.0
resolution: "eslint@npm:8.23.0"
dependencies:
"@eslint/eslintrc": ^1.3.1
"@humanwhocodes/config-array": ^0.10.4
"@humanwhocodes/gitignore-to-minimatch": ^1.0.2
"@humanwhocodes/module-importer": ^1.0.1
ajv: ^6.10.0
chalk: ^4.0.0
cross-spawn: ^7.0.2
debug: ^4.3.2
doctrine: ^3.0.0
escape-string-regexp: ^4.0.0
eslint-scope: ^7.1.1
eslint-utils: ^3.0.0
eslint-visitor-keys: ^3.3.0
espree: ^9.4.0
esquery: ^1.4.0
esutils: ^2.0.2
fast-deep-equal: ^3.1.3
file-entry-cache: ^6.0.1
find-up: ^5.0.0
functional-red-black-tree: ^1.0.1
glob-parent: ^6.0.1
globals: ^13.15.0
globby: ^11.1.0
grapheme-splitter: ^1.0.4
ignore: ^5.2.0
import-fresh: ^3.0.0
imurmurhash: ^0.1.4
is-glob: ^4.0.0
js-yaml: ^4.1.0
json-stable-stringify-without-jsonify: ^1.0.1
levn: ^0.4.1
lodash.merge: ^4.6.2
minimatch: ^3.1.2
natural-compare: ^1.4.0
optionator: ^0.9.1
regexpp: ^3.2.0
strip-ansi: ^6.0.1
strip-json-comments: ^3.1.0
text-table: ^0.2.0
bin:
eslint: bin/eslint.js
checksum: ff6075daa28d817a7ac4508f31bc108a04d9ab5056608c8651b5bf9cfea5d708ca16dea6cdab2c3c0ae99b0bf0e726af8504eaa8e17c8e12e242cb68237ead64
languageName: node
linkType: hard
"espree@npm:^7.3.0, espree@npm:^7.3.1":
version: 7.3.1
resolution: "espree@npm:7.3.1"
@@ -18059,6 +18133,17 @@ __metadata:
languageName: node
linkType: hard
"espree@npm:^9.4.0":
version: 9.4.0
resolution: "espree@npm:9.4.0"
dependencies:
acorn: ^8.8.0
acorn-jsx: ^5.3.2
eslint-visitor-keys: ^3.3.0
checksum: 2e3020dde67892d2ba3632413b44d0dc31d92c29ce72267d7ec24216a562f0a6494d3696e2fa39a3ec8c0e0088d773947ab2925fbb716801a11eb8dd313ac89c
languageName: node
linkType: hard
"esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0":
version: 4.0.1
resolution: "esprima@npm:4.0.1"